From ae3b84cf08f09d48dc193831ac4ebaef563c014c Mon Sep 17 00:00:00 2001 From: andycall Date: Thu, 26 Mar 2026 10:37:03 -0700 Subject: [PATCH 01/13] feat: add agent skills --- .../skills/webf-api-compatibility/SKILL.md | 484 ++++++ .../webf-api-compatibility/alternatives.md | 529 ++++++ .../webf-api-compatibility/reference.md | 386 +++++ .agents/skills/webf-async-rendering/SKILL.md | 317 ++++ .../skills/webf-async-rendering/examples.md | 443 +++++ .../skills/webf-infinite-scrolling/SKILL.md | 842 ++++++++++ .../webf-infinite-scrolling/examples.md | 1085 ++++++++++++ .../skills/webf-native-plugin-dev/SKILL.md | 1468 +++++++++++++++++ .agents/skills/webf-native-plugins/SKILL.md | 352 ++++ .../skills/webf-native-plugins/reference.md | 297 ++++ .agents/skills/webf-native-ui-dev/SKILL.md | 736 +++++++++ .../webf-native-ui-dev/example-input.md | 589 +++++++ .../webf-native-ui-dev/typescript-guide.md | 111 ++ .agents/skills/webf-native-ui/SKILL.md | 348 ++++ .agents/skills/webf-native-ui/reference.md | 562 +++++++ .agents/skills/webf-quickstart/SKILL.md | 222 +++ .agents/skills/webf-quickstart/reference.md | 199 +++ .agents/skills/webf-routing-setup/SKILL.md | 584 +++++++ .../webf-routing-setup/cross-platform.md | 528 ++++++ .agents/skills/webf-routing-setup/examples.md | 947 +++++++++++ 20 files changed, 11029 insertions(+) create mode 100644 .agents/skills/webf-api-compatibility/SKILL.md create mode 100644 .agents/skills/webf-api-compatibility/alternatives.md create mode 100644 .agents/skills/webf-api-compatibility/reference.md create mode 100644 .agents/skills/webf-async-rendering/SKILL.md create mode 100644 .agents/skills/webf-async-rendering/examples.md create mode 100644 .agents/skills/webf-infinite-scrolling/SKILL.md create mode 100644 .agents/skills/webf-infinite-scrolling/examples.md create mode 100644 .agents/skills/webf-native-plugin-dev/SKILL.md create mode 100644 .agents/skills/webf-native-plugins/SKILL.md create mode 100644 .agents/skills/webf-native-plugins/reference.md create mode 100644 .agents/skills/webf-native-ui-dev/SKILL.md create mode 100644 .agents/skills/webf-native-ui-dev/example-input.md create mode 100644 .agents/skills/webf-native-ui-dev/typescript-guide.md create mode 100644 .agents/skills/webf-native-ui/SKILL.md create mode 100644 .agents/skills/webf-native-ui/reference.md create mode 100644 .agents/skills/webf-quickstart/SKILL.md create mode 100644 .agents/skills/webf-quickstart/reference.md create mode 100644 .agents/skills/webf-routing-setup/SKILL.md create mode 100644 .agents/skills/webf-routing-setup/cross-platform.md create mode 100644 .agents/skills/webf-routing-setup/examples.md diff --git a/.agents/skills/webf-api-compatibility/SKILL.md b/.agents/skills/webf-api-compatibility/SKILL.md new file mode 100644 index 0000000000..1f290c3bd9 --- /dev/null +++ b/.agents/skills/webf-api-compatibility/SKILL.md @@ -0,0 +1,484 @@ +--- +name: webf-api-compatibility +description: Check Web API and CSS feature compatibility in WebF - determine what JavaScript APIs, DOM methods, CSS properties, and layout modes are supported. Use when planning features, debugging why APIs don't work, or finding alternatives for unsupported features like IndexedDB, WebGL, float layout, or CSS Grid. +--- + +# WebF API & CSS Compatibility + +> **Note**: WebF development is nearly identical to web development - you use the same tools (Vite, npm, Vitest), same frameworks (React, Vue, Svelte), and same deployment services (Vercel, Netlify). This skill covers **one of the 3 key differences**: checking API and CSS compatibility before implementation. The other two differences are async rendering and routing. + +**WebF is NOT a browser** - it's a Flutter application runtime that implements W3C/WHATWG web standards. This means some browser APIs are not available, and some CSS features work differently. + +This skill helps you quickly check what's supported and find alternatives for unsupported features. + +## Quick Compatibility Check + +When asked about a specific API or CSS feature, I will: + +1. Check if it's in the supported list +2. Provide the compatibility status (✅ Supported, ⏳ Coming Soon, ❌ Not Supported) +3. Explain why it's not supported (if applicable) +4. Suggest alternatives or workarounds + +## JavaScript & Web APIs + +### ✅ Fully Supported + +#### Timers & Animation +- `setTimeout()`, `clearTimeout()` +- `setInterval()`, `clearInterval()` +- `requestAnimationFrame()`, `cancelAnimationFrame()` + +#### Networking +- `fetch()` - Full support with async/await +- `XMLHttpRequest` - For legacy code +- `WebSocket` - Real-time bidirectional communication +- `EventSource` - Server-Sent Events (SSE) for real-time server push +- `URL` and `URLSearchParams` - URL manipulation + +#### Storage +- `localStorage` - Persistent key-value storage +- `sessionStorage` - Session-only storage + +#### Graphics +- **Canvas 2D** - Full 2D canvas API +- **SVG** - SVG element rendering + +#### DOM APIs +- `document`, `window`, `navigator` +- `querySelector()`, `querySelectorAll()` +- `addEventListener()`, `removeEventListener()` +- `createElement()`, `appendChild()`, etc. +- `MutationObserver` - Watch DOM changes +- **Custom Elements** - Define custom HTML elements + +#### Events +- `click` - Enabled by default +- Other events via `FlutterGestureDetector` (double-tap, long-press, etc.) + +### ⏳ Coming Soon + +- **CSS Grid** - Planned for future release +- **Tailwind CSS v4** - Planned for 2026 + +### ❌ NOT Supported + +#### Storage +- **IndexedDB** - Use native plugin instead + - Alternative: `sqflite`, `hive`, or custom plugin + +#### Graphics +- **WebGL** - Not available, no alternative +- **Web Animations API** (JavaScript) - CSS animations work, but not the JS API + +#### Workers +- **Web Workers** - Not needed + - Why: JavaScript already runs on dedicated thread in WebF + - No performance benefit from workers + +#### DOM +- **Shadow DOM** - Not used for component encapsulation + - Use framework component systems instead (React, Vue, etc.) + +#### Observers +- **IntersectionObserver** - Use `onscreen`/`offscreen` events instead + +## CSS Compatibility + +### ✅ Fully Supported Layout Modes + +#### Standard Flow +- `block` - Block-level elements +- `inline` - Inline elements +- `inline-block` - Inline elements with block properties + +#### Flexbox (Recommended) +- `display: flex` +- All flex properties (`justify-content`, `align-items`, `flex-direction`, etc.) +- **This is the recommended layout approach** + +#### Positioned Layout +- `position: relative` +- `position: absolute` +- `position: fixed` +- `position: sticky` + +#### Text & Direction +- RTL (right-to-left) support +- All text alignment and direction properties + +### ❌ NOT Supported Layout Modes + +#### Float Layout (Legacy) +- `float: left` / `float: right` - **NOT SUPPORTED** +- `clear` - **NOT SUPPORTED** +- **Why**: Legacy layout model, use Flexbox instead + +#### Table Layout +- `display: table` - **NOT SUPPORTED** +- `display: table-row`, `display: table-cell` - **NOT SUPPORTED** +- **Why**: Use Flexbox or CSS Grid (when available) + +### ⏳ Coming Soon + +#### CSS Grid +- `display: grid` - **Planned** +- All grid properties - **Planned** +- **Use Flexbox until Grid is available** + +### ✅ Fully Supported CSS Features + +#### Colors & Backgrounds +- All color formats (hex, rgb, rgba, hsl, hsla, named) +- `background-color`, `background-image`, `background-size`, etc. +- Linear gradients, radial gradients + +#### Borders & Shapes +- `border`, `border-radius`, `border-color`, etc. +- `box-shadow`, `text-shadow` + +#### Transforms (Hardware Accelerated) +- `transform: translate()`, `rotate()`, `scale()`, `skew()` +- 2D and 3D transforms +- **Use for smooth animations** + +#### Animations & Transitions +- `transition` - All transition properties +- `@keyframes` and `animation` +- **CSS animations are fully supported** + +#### Layout & Sizing +- `width`, `height`, `min-width`, `max-width`, etc. +- `margin`, `padding` +- `box-sizing` + +#### Responsive Design +- `@media` queries +- Viewport units (`vw`, `vh`, `vmin`, `vmax`) +- **Some advanced units not supported** (`dvh`, `lvh`, `svh`) + +#### Advanced CSS +- CSS variables (`--custom-property`) +- Pseudo-classes (`:hover`, `:active`, `:focus`, etc.) +- Pseudo-elements (`::before`, `::after`) +- Filters (`blur`, `brightness`, `contrast`, etc.) +- `z-index` and stacking contexts + +### ⚠️ Partially Supported + +#### Tailwind CSS +- **v3** - ✅ Supported (some utilities may not work if they use unsupported features) +- **v4** - ❌ Not yet supported (planned for 2026) + +### ❌ NOT Supported CSS Features + +- **`backdrop-filter`** - Not available +- **Advanced viewport units** - `dvh`, `lvh`, `svh` + +## Architecture Differences + +Understanding why some features aren't available: + +| Aspect | Browser | WebF | +|--------|---------|------| +| Runtime | V8 / SpiderMonkey | QuickJS (ES6+) | +| DOM | Blink / Gecko | Custom C++ + Dart | +| Layout | Browser engine | Flutter rendering | +| Purpose | General web browsing | App runtime | + +**Key Insight**: WebF implements core web standards for building apps, not for web browsing. + +## Finding Alternatives + +### For IndexedDB → Use Native Storage + +```javascript +// ❌ IndexedDB not available +// const db = await openDB('mydb', 1); + +// ✅ Option 1: localStorage for simple key-value storage (RECOMMENDED for most cases) +localStorage.setItem('user', JSON.stringify({ name: 'Alice', age: 30 })); +const user = JSON.parse(localStorage.getItem('user')); + +// ✅ Option 2: Request custom native plugin from Flutter team +// For complex database needs (SQL, large datasets, queries): +// - Flutter team creates native plugin using sqflite, Hive, or Isar +// - Plugin exposed to JavaScript via WebF module system +// - See: https://openwebf.com/en/docs/add-webf-to-flutter/bridge-modules + +// Example of custom storage plugin (created by Flutter team): +// import { AppStorage } from '@yourapp/storage-plugin'; +// await AppStorage.save('key', { complex: 'data' }); +// const data = await AppStorage.get('key'); +``` + +### For WebGL → No Alternative + +```javascript +// ❌ WebGL not available +// const gl = canvas.getContext('webgl'); + +// ✅ Use Canvas 2D instead (limited graphics) +const ctx = canvas.getContext('2d'); + +// ✅ Or use Flutter's rendering for complex graphics +// Flutter team can render directly +``` + +### For Float Layout → Use Flexbox + +```css +/* ❌ Float layout not supported */ +.sidebar { float: left; width: 200px; } +.content { float: right; width: calc(100% - 200px); } + +/* ✅ Use Flexbox instead */ +.container { display: flex; } +.sidebar { width: 200px; flex-shrink: 0; } +.content { flex-grow: 1; } +``` + +### For Table Layout → Use Flexbox + +```css +/* ❌ Table layout not supported */ +.table { display: table; } +.row { display: table-row; } +.cell { display: table-cell; } + +/* ✅ Use Flexbox instead */ +.table { display: flex; flex-direction: column; } +.row { display: flex; } +.cell { flex: 1; } +``` + +### For Native Device Features → Use WebF Plugins + +**Check available plugins**: https://openwebf.com/en/native-plugins + +WebF provides official npm packages for native features. When you need a native feature: + +1. **First, check the available plugins list** at https://openwebf.com/en/native-plugins +2. **Ask the user about their environment** before providing setup instructions +3. **Follow the plugin's installation guide** for their specific environment + +#### Step 1: Determine Your Environment + +**IMPORTANT**: Setup differs based on your development environment. + +**Question**: "Are you testing in WebF Go, or working on a production app?" + +**Option 1: Testing in WebF Go** (Most web developers) +- ✅ Just install npm package +- ✅ No additional setup needed +- Example: `npm install @openwebf/webf-share` + +**Option 2: Production app with Flutter team** +- ⚠️ Your Flutter developer needs to add the Flutter plugin first +- ⚠️ Once they've done that, you install the npm package +- ⚠️ Coordinate with your Flutter team - give them the plugin documentation + +#### Example: Native Share Plugin + +**If using WebF Go:** +```bash +# Just install npm package +npm install @openwebf/webf-share +``` + +**If integrating with Flutter app:** +```bash +# 1. Add Flutter plugin first (in pubspec.yaml) +# See: https://openwebf.com/en/native-plugins/webf-share + +# 2. Then install npm package +npm install @openwebf/webf-share +``` + +**Usage in JavaScript:** +```javascript +import { WebFShare } from '@openwebf/webf-share'; + +// Check availability first +if (WebFShare.isAvailable()) { + // Share text + await WebFShare.shareText({ + text: 'Check this out!', + url: 'https://example.com', + title: 'My App' + }); +} + +// Or use React hook +import { useWebFShare } from '@openwebf/webf-share'; + +function ShareButton() { + const { share, isAvailable } = useWebFShare(); + + if (!isAvailable) return null; + + return ( + + ); +} +``` + +#### Finding the Right Plugin + +When looking for native features: + +1. **Check plugin list**: https://openwebf.com/en/native-plugins +2. **If plugin exists**: Follow its installation guide +3. **If no plugin exists**: + - For WebF Go users: Feature may not be available + - For Flutter integration: Create custom plugin using [WebF Module System](/docs/add-webf-to-flutter/bridge-modules) + +## How to Check Compatibility + +### 1. Check the Compatibility Table + +See `reference.md` in this skill for complete compatibility tables. + +### 2. Test in WebF Go + +```javascript +// Quick compatibility test +if (typeof IndexedDB !== 'undefined') { + console.log('IndexedDB available'); +} else { + console.log('IndexedDB NOT available - use alternative'); +} + +// Check for WebF-specific features +if (typeof WebF !== 'undefined') { + console.log('Running in WebF'); +} +``` + +### 3. Use Feature Detection + +```javascript +function checkStorageOptions() { + const support = { + localStorage: typeof localStorage !== 'undefined', + sessionStorage: typeof sessionStorage !== 'undefined', + indexedDB: typeof indexedDB !== 'undefined' + }; + + console.log('Storage support:', support); + return support; +} +``` + +## Common Questions + +### "Can I use Tailwind CSS?" + +**Yes, but only v3** - v4 is planned for 2026. + +```bash +# Install Tailwind v3 +npm install -D tailwindcss@^3.0 postcss autoprefixer +``` + +Some Tailwind utilities may not work if they use unsupported CSS features (like float or table layout). + +### "Why no Web Workers?" + +JavaScript in WebF already runs on a **dedicated thread**, separate from the Flutter UI thread. Web Workers would provide no performance benefit. + +### "Can I use React Query / SWR / Axios?" + +**Yes!** All popular libraries that use `fetch()` or `XMLHttpRequest` work perfectly: + +- ✅ React Query +- ✅ SWR +- ✅ Axios +- ✅ TanStack Query +- ✅ Apollo Client (if using fetch) + +### "Can I use AI streaming APIs (OpenAI, etc.)?" + +**Yes!** WebF supports both `EventSource` (SSE) and `fetch()` streaming, which are the two main approaches used by AI SDKs: + +- ✅ OpenAI streaming (uses SSE) +- ✅ Vercel AI SDK (uses SSE / fetch streaming) +- ✅ Any SSE-based streaming API +- ✅ `EventSource` with named events, auto-reconnect, and `lastEventId` + +### "What about CSS-in-JS libraries?" + +Most work fine as they generate standard CSS: + +- ✅ styled-components +- ✅ Emotion +- ✅ CSS Modules +- ✅ Sass/SCSS + +### "Can I use CSS Grid?" + +**Not yet** - it's coming soon. Use Flexbox for now, which handles most layouts. + +## Debugging Compatibility Issues + +If a feature isn't working: + +1. **Check this compatibility guide** - Is it supported? +2. **Test in browser first** - Does it work in a regular browser? +3. **Check for typos** - Correct API names? +4. **Read error messages** - WebF provides helpful errors +5. **Use feature detection** - Check if API exists before using + +```javascript +// Good practice: feature detection +if (typeof fetch !== 'undefined') { + // Use fetch +} else { + // Fallback or error message + console.error('fetch not available'); +} +``` + +## Resources + +- **API Compatibility Table**: https://openwebf.com/en/docs/developer-guide/core-concepts#api-compatibility +- **CSS Support Overview**: https://openwebf.com/en/docs/developer-guide/css +- **CSS Layout Support**: https://openwebf.com/en/docs/learn-webf/key-features#css-layout +- **Native Plugins Guide**: https://openwebf.com/en/docs/developer-guide/native-plugins +- **Complete Reference**: See `reference.md` for detailed tables + +## Quick Decision Tree + +``` +Need storage? +├─ Simple key-value → localStorage ✅ +├─ Complex database → Native plugin (sqflite/hive) ✅ +└─ IndexedDB → ❌ Not available + +Need layout? +├─ Flexible layout → Flexbox ✅ +├─ Grid layout → Wait for CSS Grid ⏳ or use Flexbox +├─ Float layout → ❌ Use Flexbox instead +└─ Table layout → ❌ Use Flexbox instead + +Need graphics? +├─ 2D canvas → Canvas 2D ✅ +├─ SVG → SVG ✅ +├─ 3D graphics → ❌ WebGL not available +└─ Complex graphics → Flutter rendering ✅ + +Need networking? +├─ HTTP requests → fetch ✅ +├─ Real-time (bidirectional) → WebSocket ✅ +├─ Real-time (server push) → EventSource (SSE) ✅ +├─ AI streaming → EventSource or fetch ✅ +└─ GraphQL → fetch-based clients ✅ + +Need native features? +├─ Share → @openwebf/webf-share ✅ +├─ Camera → Native plugin ✅ +├─ Storage → Native plugin ✅ +└─ Custom → Flutter team creates plugin ✅ +``` \ No newline at end of file diff --git a/.agents/skills/webf-api-compatibility/alternatives.md b/.agents/skills/webf-api-compatibility/alternatives.md new file mode 100644 index 0000000000..950f70b19c --- /dev/null +++ b/.agents/skills/webf-api-compatibility/alternatives.md @@ -0,0 +1,529 @@ +# WebF API Alternatives & Native Plugins + +When WebF doesn't support a browser API, you have several options: use a simpler supported API, use an official WebF plugin, or work with the Flutter team to create a custom native plugin. + +## Storage Alternatives + +### IndexedDB → localStorage or Native Plugin + +**IndexedDB is NOT supported in WebF.** Here are your alternatives: + +#### Option 1: Use localStorage (Recommended for Simple Cases) + +For most applications, `localStorage` provides sufficient storage: + +```javascript +// ✅ Simple key-value storage with JSON +const user = { + id: 123, + name: 'Alice', + preferences: { + theme: 'dark', + language: 'en' + } +}; + +// Store +localStorage.setItem('user', JSON.stringify(user)); + +// Retrieve +const storedUser = JSON.parse(localStorage.getItem('user')); + +// Remove +localStorage.removeItem('user'); + +// Clear all +localStorage.clear(); + +// Check existence +if (localStorage.getItem('user') !== null) { + console.log('User data exists'); +} +``` + +**Advantages**: +- ✅ Synchronous API (simple to use) +- ✅ Supported everywhere +- ✅ No external dependencies +- ✅ ~5-10MB storage limit (platform-dependent) + +**Limitations**: +- ❌ No indexing or queries +- ❌ No transactions +- ❌ String-only storage (must serialize) +- ❌ Synchronous (blocks thread for large data) + +#### Option 2: Request Custom Native Plugin (For Complex Needs) + +For complex database requirements (SQL queries, large datasets, relationships), work with the Flutter team to create a custom plugin using WebF's module system. + +**When to use native plugins**: +- Large datasets (> 10MB) +- Complex queries (JOIN, WHERE, ORDER BY) +- Relational data +- High-performance requirements +- Offline-first applications + +**Example workflow**: +1. **Flutter team creates native plugin** using `sqflite`, `Hive`, or `Isar` +2. **Plugin exposed to JavaScript** via WebF module system +3. **JavaScript code uses plugin** through simple async API + +```javascript +// Example of custom storage plugin (created by Flutter team) +// This is NOT a real package, just an example pattern + +import { AppStorage } from '@yourapp/storage-plugin'; + +// Initialize +await AppStorage.init({ dbName: 'myapp', version: 1 }); + +// Save data +await AppStorage.save('users', { + id: 123, + name: 'Alice', + email: 'alice@example.com' +}); + +// Query data +const users = await AppStorage.query('users', { + where: { age: { gte: 18 } }, + orderBy: 'name', + limit: 10 +}); + +// Update +await AppStorage.update('users', { id: 123 }, { name: 'Alicia' }); + +// Delete +await AppStorage.delete('users', { id: 123 }); +``` + +**Resources**: +- WebF Module System: https://openwebf.com/en/docs/add-webf-to-flutter/bridge-modules +- Popular Flutter storage options: + - `sqflite` - SQLite database + - `Hive` - Fast key-value store + - `Isar` - High-performance NoSQL database + +## Graphics Alternatives + +### WebGL → Canvas 2D (Limited) or Flutter Rendering + +**WebGL is NOT supported in WebF.** No WebGL alternative exists within the JavaScript environment. + +#### Option 1: Use Canvas 2D (Limited Graphics) + +For simple 2D graphics, use Canvas 2D: + +```javascript +const canvas = document.getElementById('myCanvas'); +const ctx = canvas.getContext('2d'); + +// Draw shapes +ctx.fillStyle = '#FF0000'; +ctx.fillRect(10, 10, 100, 100); + +ctx.beginPath(); +ctx.arc(75, 75, 50, 0, Math.PI * 2); +ctx.fill(); + +// Draw images +const img = new Image(); +img.onload = () => { + ctx.drawImage(img, 0, 0); +}; +img.src = 'image.png'; +``` + +**What Canvas 2D can do**: +- ✅ Draw shapes (rectangles, circles, paths) +- ✅ Draw images and sprites +- ✅ Apply transformations (rotate, scale, translate) +- ✅ Gradients and patterns +- ✅ Text rendering +- ✅ Pixel manipulation + +**What Canvas 2D cannot do**: +- ❌ 3D graphics +- ❌ Hardware-accelerated shaders +- ❌ Complex lighting and textures +- ❌ High-performance game rendering + +#### Option 2: Use SVG + +For vector graphics and icons: + +```javascript +// Create SVG dynamically +const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); +svg.setAttribute('width', '200'); +svg.setAttribute('height', '200'); + +const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); +circle.setAttribute('cx', '100'); +circle.setAttribute('cy', '100'); +circle.setAttribute('r', '50'); +circle.setAttribute('fill', 'blue'); + +svg.appendChild(circle); +document.body.appendChild(svg); +``` + +#### Option 3: Flutter Rendering (Flutter Team) + +For complex graphics requirements (3D, games, custom rendering), the Flutter team can render directly using Flutter's rendering engine and expose controls to JavaScript. + +## Native Feature Plugins + +**IMPORTANT**: Always check the official plugins list first: https://openwebf.com/en/native-plugins + +### Finding Available Plugins + +Before implementing native features, follow these steps: + +1. **Visit https://openwebf.com/en/native-plugins** - Check the complete list of available plugins +2. **Ask the user about their environment** - Setup differs significantly based on where the app runs +3. **Follow the plugin's installation guide** for their specific environment + +### Step 1: Determine Your Environment + +**IMPORTANT**: Setup differs based on where you're developing. + +**Question**: "Are you testing in WebF Go, or working on a production app?" + +**Option 1: Testing in WebF Go** (Most web developers) +- ✅ Just install npm package +- ✅ No additional setup needed +- ✅ WebF Go already has Flutter plugins included + +**Example**: +```bash +# Just install npm package +npm install @openwebf/webf-share +``` + +**Option 2: Production app with Flutter team** +- ⚠️ Your Flutter developer must add the Flutter plugin first +- ⚠️ Once they confirm it's added, you can install npm package +- ⚠️ Give them the plugin documentation link + +**What to tell your Flutter developer**: +``` +"We need the webf_share plugin. Please add it to pubspec.yaml: +See: https://openwebf.com/en/native-plugins/webf-share" +``` + +**After Flutter developer confirms**: +```bash +# Then you install npm package in your JS project +npm install @openwebf/webf-share +``` + +### Example Plugin: @openwebf/webf-share + +Native share dialog for sharing content. + +**Installation:** + +**If using WebF Go:** +```bash +# Just install npm package +npm install @openwebf/webf-share +``` + +**If integrating with Flutter app:** +```bash +# 1. First add to pubspec.yaml (see plugin docs) +# 2. Run: flutter pub get +# 3. Then install npm package: +npm install @openwebf/webf-share +``` + +```javascript +import { WebFShare } from '@openwebf/webf-share'; + +// Check if sharing is available +if (WebFShare.isAvailable()) { + // Share text and URL + await WebFShare.shareText({ + text: 'Check out this awesome app!', + url: 'https://example.com', + title: 'My App' + }); +} + +// Share files (if supported by platform) +await WebFShare.shareFiles({ + files: [{ uri: 'file:///path/to/image.png', mimeType: 'image/png' }], + text: 'Check out this image' +}); +``` + +**React Hook**: + +```jsx +import { useWebFShare } from '@openwebf/webf-share'; + +function ShareButton() { + const { share, isAvailable } = useWebFShare(); + + if (!isAvailable) { + return null; // Share not available on this platform + } + + const handleShare = async () => { + try { + await share({ + text: 'Hello from WebF!', + url: 'https://openwebf.com' + }); + console.log('Shared successfully'); + } catch (error) { + console.error('Share failed:', error); + } + }; + + return ( + + ); +} +``` + +### @openwebf/webf-deeplink - Deep Linking + +Handle deep links and universal links to integrate with other apps. + +```bash +npm install @openwebf/webf-deeplink +``` + +```javascript +import { WebFDeepLink } from '@openwebf/webf-deeplink'; + +// Open a deep link with fallback +await WebFDeepLink.openDeepLink({ + url: 'whatsapp://send?text=Hello', + fallbackUrl: 'https://wa.me/?text=Hello' // Used if app not installed +}); + +// Open app settings +await WebFDeepLink.openDeepLink({ + url: 'app-settings://' +}); + +// Listen for incoming deep links (when your app is opened via deep link) +WebFDeepLink.onDeepLink((url) => { + console.log('Received deep link:', url); + + // Parse and handle the link + if (url.startsWith('myapp://product/')) { + const productId = url.split('/').pop(); + // Navigate to product page + WebFRouter.pushState({}, `/product/${productId}`); + } +}); + +// Remove listener when done +WebFDeepLink.removeDeepLinkListener(); +``` + +**Use cases**: +- Open external apps (WhatsApp, Maps, Phone dialer) +- Handle incoming deep links from other apps +- Navigate to app settings +- Universal links for app-web integration + +## Creating Custom Native Plugins + +For features not covered by official plugins, you can create custom plugins using WebF's module system. + +### Common Plugin Use Cases + +- **Camera & Media**: + - Photo/video capture + - Gallery access + - Image processing + +- **Sensors**: + - GPS/Location + - Accelerometer + - Gyroscope + - Barcode scanning + +- **System Integration**: + - Notifications + - Contacts + - Calendar + - File system access + - Bluetooth + +- **Advanced Storage**: + - SQLite database + - Secure storage + - File encryption + +### Plugin Development Process + +1. **Flutter Team Creates Plugin**: + - Implements feature using Flutter packages + - Exposes API via WebF module system + - Packages as npm module + +2. **JavaScript Integration**: + ```javascript + // Example custom plugin pattern + import { CameraPlugin } from '@yourapp/camera-plugin'; + + // Take photo + const photo = await CameraPlugin.takePicture({ + quality: 80, + cameraDirection: 'back' + }); + + // Use the photo + const img = document.createElement('img'); + img.src = photo.uri; + document.body.appendChild(img); + ``` + +3. **TypeScript Types** (optional but recommended): + ```typescript + declare module '@yourapp/camera-plugin' { + export interface CameraOptions { + quality?: number; + cameraDirection?: 'front' | 'back'; + maxWidth?: number; + maxHeight?: number; + } + + export interface Photo { + uri: string; + width: number; + height: number; + } + + export class CameraPlugin { + static takePicture(options?: CameraOptions): Promise; + static requestPermission(): Promise; + } + } + ``` + +### Module System Documentation + +**Complete guide**: https://openwebf.com/en/docs/add-webf-to-flutter/bridge-modules + +**Key concepts**: +- Plugins are Dart classes exposed to JavaScript +- Async/await supported +- Callbacks supported +- Type conversion automatic (JSON, primitives) +- Error handling built-in + +## Web Workers → Not Needed + +**Web Workers are NOT supported and NOT needed in WebF.** + +### Why Web Workers Aren't Needed + +In browsers, JavaScript runs on the main UI thread. Web Workers allow offloading work to background threads to keep the UI responsive. + +**In WebF, JavaScript already runs on a dedicated thread**, separate from the Flutter UI thread. This means: + +- ✅ JavaScript doesn't block the UI +- ✅ Heavy computations don't freeze the app +- ✅ Async operations are naturally non-blocking +- ✅ No performance benefit from Web Workers + +### Alternative Pattern + +Instead of Web Workers, use standard async patterns: + +```javascript +// Browser pattern (Web Workers) +// ❌ Not needed in WebF + +// WebF pattern (async/await) +// ✅ Works perfectly +async function heavyComputation() { + const data = await fetch('/api/large-dataset'); + const processed = await processData(data); + return processed; +} + +// Use in component +async function handleClick() { + const result = await heavyComputation(); + updateUI(result); +} +``` + +## Comparison Table: Alternatives + +| Browser API | WebF Status | Alternative | Complexity | +|-------------|-------------|-------------|------------| +| IndexedDB | ❌ | localStorage | Easy | +| IndexedDB | ❌ | Native plugin (SQLite/Hive) | Medium | +| WebGL | ❌ | Canvas 2D (limited) | Easy | +| WebGL | ❌ | Flutter rendering | Hard | +| Web Workers | ❌ | Not needed (JS on dedicated thread) | N/A | +| Web Share API | ❌ | @openwebf/webf-share | Easy | +| Geolocation | ⚠️ | Native plugin | Medium | +| Camera API | ❌ | Native plugin | Medium | +| Notifications | ❌ | Native plugin | Medium | +| Bluetooth | ❌ | Native plugin | Hard | +| File System Access | ❌ | Native plugin | Medium | + +## Decision Tree + +``` +Need storage? +├─ Simple key-value (< 5MB) +│ └─ Use localStorage ✅ +├─ Complex queries, large data +│ └─ Request native plugin (SQLite/Hive) ✅ +└─ IndexedDB specifically required + └─ ❌ Not available - refactor to use alternative + +Need graphics? +├─ 2D drawing, charts, sprites +│ └─ Use Canvas 2D ✅ +├─ Vector graphics, icons +│ └─ Use SVG ✅ +├─ 3D graphics, WebGL +│ └─ ❌ Not available - discuss with Flutter team + +Need native features? +├─ Share content +│ └─ @openwebf/webf-share ✅ +├─ Deep linking +│ └─ @openwebf/webf-deeplink ✅ +├─ Camera, GPS, notifications, etc. +│ └─ Request custom plugin ✅ + +Need background threads? +└─ Web Workers + └─ ❌ Not needed - JS runs on dedicated thread +``` + +## Resources + +- **Official Plugins List**: https://openwebf.com/en/native-plugins (Check here first!) +- **WebF Plugins on npm**: https://www.npmjs.com/search?q=%40openwebf +- **Module System Guide**: https://openwebf.com/en/docs/add-webf-to-flutter/bridge-modules +- **Flutter Packages**: https://pub.dev (for Flutter team to use when building plugins) +- **GitHub Discussions**: Ask about plugin development + +## Summary + +When you encounter an unsupported API: + +1. **Check if a simpler API works** (e.g., localStorage instead of IndexedDB) +2. **Look for official WebF plugins** (@openwebf/webf-share, @openwebf/webf-deeplink) +3. **Request custom plugin** for complex native features +4. **Consider if the feature is truly needed** (e.g., Web Workers aren't necessary) + +Most web applications can be built with the supported APIs. For native features, WebF's plugin system provides a bridge between JavaScript and native capabilities. \ No newline at end of file diff --git a/.agents/skills/webf-api-compatibility/reference.md b/.agents/skills/webf-api-compatibility/reference.md new file mode 100644 index 0000000000..7c6c2608d2 --- /dev/null +++ b/.agents/skills/webf-api-compatibility/reference.md @@ -0,0 +1,386 @@ +# WebF API & CSS Compatibility Reference + +Complete compatibility tables for quick reference when building WebF applications. + +## JavaScript & Web APIs + +### ✅ Fully Supported APIs + +#### Timers & Animation +| API | Status | Notes | +|-----|--------|-------| +| `setTimeout()` | ✅ | Full support | +| `clearTimeout()` | ✅ | Full support | +| `setInterval()` | ✅ | Full support | +| `clearInterval()` | ✅ | Full support | +| `requestAnimationFrame()` | ✅ | Full support, use for smooth animations | +| `cancelAnimationFrame()` | ✅ | Full support | + +#### Storage APIs +| API | Status | Notes | +|-----|--------|-------| +| `localStorage` | ✅ | Persistent key-value storage | +| `sessionStorage` | ✅ | Session-only storage | +| `IndexedDB` | ❌ | Not supported - use native plugin | + +#### Networking +| API | Status | Notes | +|-----|--------|-------| +| `fetch()` | ✅ | Full async/await support | +| `XMLHttpRequest` | ✅ | For legacy code | +| `WebSocket` | ✅ | Real-time bidirectional communication | +| `EventSource` | ✅ | Server-Sent Events (SSE) — server push, auto-reconnect, named events | +| `URL` | ✅ | URL parsing and manipulation | +| `URLSearchParams` | ✅ | Query string handling | + +#### Graphics APIs +| API | Status | Notes | +|-----|--------|-------| +| Canvas 2D | ✅ | Full 2D canvas API | +| SVG | ✅ | SVG element rendering | +| WebGL | ❌ | Not available, no alternative | +| WebGL2 | ❌ | Not available, no alternative | + +#### DOM APIs +| API | Status | Notes | +|-----|--------|-------| +| `document.*` | ✅ | Standard DOM APIs | +| `window.*` | ✅ | Standard window APIs | +| `navigator.*` | ✅ | Navigator object | +| `querySelector()` | ✅ | CSS selector queries | +| `querySelectorAll()` | ✅ | CSS selector queries | +| `getElementById()` | ✅ | ID-based lookup | +| `getElementsByClassName()` | ✅ | Class-based lookup | +| `getElementsByTagName()` | ✅ | Tag-based lookup | +| `createElement()` | ✅ | Create elements | +| `appendChild()` | ✅ | DOM manipulation | +| `removeChild()` | ✅ | DOM manipulation | +| `insertBefore()` | ✅ | DOM manipulation | +| `cloneNode()` | ✅ | Node cloning | +| Custom Elements | ✅ | Define custom HTML elements | +| Shadow DOM | ❌ | Not supported - use framework components | + +#### Event APIs +| API | Status | Notes | +|-----|--------|-------| +| `addEventListener()` | ✅ | Standard event handling | +| `removeEventListener()` | ✅ | Standard event handling | +| `click` events | ✅ | Enabled by default | +| `input` events | ✅ | Form input handling | +| `change` events | ✅ | Form change handling | +| `submit` events | ✅ | Form submission | +| `onscreen` | ✅ | WebF-specific: element laid out | +| `offscreen` | ✅ | WebF-specific: element removed | +| Other gestures | ⚠️ | Via `FlutterGestureDetector` | + +#### Observers +| API | Status | Notes | +|-----|--------|-------| +| `MutationObserver` | ✅ | Watch DOM changes | +| `IntersectionObserver` | ❌ | Use `onscreen`/`offscreen` events | +| `ResizeObserver` | ❌ | Not supported | + +#### Workers & Threads +| API | Status | Notes | +|-----|--------|-------| +| Web Workers | ❌ | Not needed - JS runs on dedicated thread | +| Service Workers | ❌ | Not supported | +| Shared Workers | ❌ | Not supported | + +#### Animation APIs +| API | Status | Notes | +|-----|--------|-------| +| CSS Animations | ✅ | `@keyframes`, `animation` property | +| CSS Transitions | ✅ | `transition` property | +| Web Animations API (JS) | ❌ | Not supported - use CSS animations | + +### ⏳ Coming Soon + +| API | Status | Expected | +|-----|--------|----------| +| CSS Grid | ⏳ | Future release | +| Tailwind CSS v4 | ⏳ | 2026 | + +## CSS Compatibility + +### Layout Modes + +#### ✅ Fully Supported +| Layout Mode | Support | Notes | +|-------------|---------|-------| +| Block | ✅ | `display: block` | +| Inline | ✅ | `display: inline` | +| Inline-block | ✅ | `display: inline-block` | +| Flexbox | ✅ | **Recommended** - Full support | +| Positioned (relative) | ✅ | `position: relative` | +| Positioned (absolute) | ✅ | `position: absolute` | +| Positioned (fixed) | ✅ | `position: fixed` | +| Positioned (sticky) | ✅ | `position: sticky` | + +#### ❌ NOT Supported +| Layout Mode | Support | Alternative | +|-------------|---------|-------------| +| Float | ❌ | Use Flexbox | +| Table layout | ❌ | Use Flexbox or CSS Grid (when available) | +| CSS Grid | ⏳ | Coming soon - use Flexbox for now | + +### CSS Properties + +#### Colors & Backgrounds +| Property | Support | Notes | +|----------|---------|-------| +| `color` | ✅ | All formats (hex, rgb, rgba, hsl, hsla, named) | +| `background-color` | ✅ | All color formats | +| `background-image` | ✅ | Including gradients | +| `background-size` | ✅ | `cover`, `contain`, dimensions | +| `background-position` | ✅ | Full support | +| `background-repeat` | ✅ | Full support | +| `linear-gradient()` | ✅ | Linear gradients | +| `radial-gradient()` | ✅ | Radial gradients | +| `opacity` | ✅ | Full support | + +#### Borders & Shapes +| Property | Support | Notes | +|----------|---------|-------| +| `border` | ✅ | All border properties | +| `border-radius` | ✅ | Rounded corners | +| `border-color` | ✅ | Per-side colors | +| `border-width` | ✅ | Per-side widths | +| `border-style` | ✅ | Solid, dashed, dotted, etc. | +| `box-shadow` | ✅ | Multiple shadows supported | +| `text-shadow` | ✅ | Text shadows | +| `outline` | ✅ | Full support | + +#### Transforms (Hardware Accelerated) +| Property | Support | Notes | +|----------|---------|-------| +| `transform: translate()` | ✅ | 2D and 3D | +| `transform: rotate()` | ✅ | 2D and 3D | +| `transform: scale()` | ✅ | 2D and 3D | +| `transform: skew()` | ✅ | 2D | +| `transform-origin` | ✅ | Full support | +| `perspective` | ✅ | 3D transforms | + +#### Animations & Transitions +| Property | Support | Notes | +|----------|---------|-------| +| `transition` | ✅ | All properties | +| `transition-duration` | ✅ | Timing | +| `transition-timing-function` | ✅ | Easing functions | +| `transition-delay` | ✅ | Delays | +| `@keyframes` | ✅ | Animation definitions | +| `animation` | ✅ | All properties | +| `animation-duration` | ✅ | Timing | +| `animation-timing-function` | ✅ | Easing functions | +| `animation-delay` | ✅ | Delays | +| `animation-iteration-count` | ✅ | Repeat counts | +| `animation-direction` | ✅ | Forward, reverse, alternate | + +#### Layout & Sizing +| Property | Support | Notes | +|----------|---------|-------| +| `width` / `height` | ✅ | All units | +| `min-width` / `min-height` | ✅ | Constraints | +| `max-width` / `max-height` | ✅ | Constraints | +| `margin` | ✅ | All sides | +| `padding` | ✅ | All sides | +| `box-sizing` | ✅ | `border-box`, `content-box` | +| `overflow` | ✅ | `visible`, `hidden`, `scroll`, `auto` | +| `display` | ⚠️ | Most values (see layout table) | + +#### Flexbox Properties +| Property | Support | Notes | +|----------|---------|-------| +| `display: flex` | ✅ | Primary layout mode | +| `flex-direction` | ✅ | `row`, `column`, `row-reverse`, `column-reverse` | +| `justify-content` | ✅ | Main axis alignment | +| `align-items` | ✅ | Cross axis alignment | +| `align-content` | ✅ | Multi-line alignment | +| `flex-wrap` | ✅ | `wrap`, `nowrap`, `wrap-reverse` | +| `flex-grow` | ✅ | Grow factor | +| `flex-shrink` | ✅ | Shrink factor | +| `flex-basis` | ✅ | Base size | +| `gap` | ✅ | Spacing between items | +| `align-self` | ✅ | Individual item alignment | +| `order` | ✅ | Item ordering | + +#### Positioning +| Property | Support | Notes | +|----------|---------|-------| +| `position` | ✅ | `static`, `relative`, `absolute`, `fixed`, `sticky` | +| `top` / `right` / `bottom` / `left` | ✅ | Positioning offsets | +| `z-index` | ✅ | Stacking order | + +#### Text & Typography +| Property | Support | Notes | +|----------|---------|-------| +| `font-family` | ✅ | Web fonts and system fonts | +| `font-size` | ✅ | All units | +| `font-weight` | ✅ | Numeric and keywords | +| `font-style` | ✅ | `normal`, `italic`, `oblique` | +| `line-height` | ✅ | All units | +| `letter-spacing` | ✅ | Character spacing | +| `word-spacing` | ✅ | Word spacing | +| `text-align` | ✅ | All alignments | +| `text-decoration` | ✅ | Underline, overline, line-through | +| `text-transform` | ✅ | `uppercase`, `lowercase`, `capitalize` | +| `white-space` | ✅ | Text wrapping control | +| `text-overflow` | ✅ | `ellipsis` support | + +#### Responsive Design +| Property | Support | Notes | +|----------|---------|-------| +| `@media` queries | ✅ | Full support | +| `vw` / `vh` | ✅ | Viewport units | +| `vmin` / `vmax` | ✅ | Viewport min/max | +| `dvh` / `lvh` / `svh` | ❌ | Advanced viewport units not supported | +| `rem` | ✅ | Root em units | +| `em` | ✅ | Em units | +| `%` | ✅ | Percentage units | + +#### Advanced CSS +| Property | Support | Notes | +|----------|---------|-------| +| CSS Variables (`--custom`) | ✅ | Custom properties | +| `calc()` | ✅ | Mathematical calculations | +| `var()` | ✅ | Variable references | +| Pseudo-classes (`:hover`, `:active`, etc.) | ✅ | Interactive states | +| Pseudo-elements (`::before`, `::after`) | ✅ | Generated content | +| Filters (`blur`, `brightness`, etc.) | ✅ | Visual effects | +| `backdrop-filter` | ❌ | Not supported | +| `clip-path` | ✅ | Shape clipping | +| `mask` | ⚠️ | Partial support | + +### CSS Frameworks + +| Framework | Version | Support | Notes | +|-----------|---------|---------|-------| +| Tailwind CSS | v3.x | ✅ | Some utilities may not work if using unsupported features | +| Tailwind CSS | v4.x | ❌ | Planned for 2026 | +| Bootstrap | All | ✅ | Works with caveats (no float-based grid) | +| Material-UI | All | ✅ | Full support | +| Ant Design | All | ✅ | Full support | +| Chakra UI | All | ✅ | Full support | + +## Popular Libraries + +### State Management +| Library | Support | Notes | +|---------|---------|-------| +| Redux | ✅ | Full support | +| Zustand | ✅ | Full support | +| Jotai | ✅ | Full support | +| Recoil | ✅ | Full support | +| MobX | ✅ | Full support | + +### Data Fetching & Streaming +| Library | Support | Notes | +|---------|---------|-------| +| React Query (TanStack Query) | ✅ | Full support | +| SWR | ✅ | Full support | +| Axios | ✅ | Full support | +| Apollo Client | ✅ | Works if using fetch transport | +| Vercel AI SDK | ✅ | SSE streaming via EventSource | +| OpenAI SDK (streaming) | ✅ | SSE streaming supported | + +### CSS-in-JS +| Library | Support | Notes | +|---------|---------|-------| +| styled-components | ✅ | Full support | +| Emotion | ✅ | Full support | +| CSS Modules | ✅ | Full support | +| Sass/SCSS | ✅ | Full support | +| Styled-JSX | ✅ | Full support | + +### UI Component Libraries +| Library | Support | Notes | +|---------|---------|-------| +| Material-UI (MUI) | ✅ | Full support | +| Ant Design | ✅ | Full support | +| Chakra UI | ✅ | Full support | +| Mantine | ✅ | Full support | +| Radix UI | ✅ | Full support | +| Headless UI | ✅ | Full support | + +## Feature Detection Template + +Use this template to check compatibility at runtime: + +```javascript +const webfFeatures = { + // Storage + localStorage: typeof localStorage !== 'undefined', + sessionStorage: typeof sessionStorage !== 'undefined', + indexedDB: typeof indexedDB !== 'undefined', // Will be false + + // Graphics + canvas2d: (() => { + const canvas = document.createElement('canvas'); + return !!(canvas.getContext && canvas.getContext('2d')); + })(), + webgl: (() => { + const canvas = document.createElement('canvas'); + return !!(canvas.getContext && canvas.getContext('webgl')); // Will be false + })(), + + // Networking + fetch: typeof fetch !== 'undefined', + websocket: typeof WebSocket !== 'undefined', + eventSource: typeof EventSource !== 'undefined', + + // Workers + webWorkers: typeof Worker !== 'undefined', // Will be false + + // Observers + mutationObserver: typeof MutationObserver !== 'undefined', + intersectionObserver: typeof IntersectionObserver !== 'undefined', // Will be false + + // WebF-specific + isWebF: typeof WebF !== 'undefined', + webfEvents: typeof WebF !== 'undefined' && 'onscreen' in document.createElement('div') +}; + +console.log('WebF Features:', webfFeatures); +``` + +## Quick Decision Matrix + +### "Which storage API should I use?" +| Use Case | Solution | +|----------|----------| +| Simple key-value (< 5MB) | `localStorage` ✅ | +| Session-only data | `sessionStorage` ✅ | +| Complex queries, large datasets | Native plugin (sqflite, hive) ✅ | +| IndexedDB required | ❌ Not available - use native alternative | + +### "Which layout approach should I use?" +| Use Case | Solution | +|----------|----------| +| Modern responsive layout | Flexbox ✅ | +| Complex grid layouts | Wait for CSS Grid ⏳ or use Flexbox | +| Legacy float layout | ❌ Convert to Flexbox | +| Table-based layout | ❌ Convert to Flexbox | + +### "Which graphics API should I use?" +| Use Case | Solution | +|----------|----------| +| 2D graphics, charts | Canvas 2D ✅ | +| Icons, illustrations | SVG ✅ | +| 3D graphics, WebGL | ❌ Not available - consider Flutter rendering | + +### "Which framework should I use?" +| Framework | Status | +|-----------|--------| +| React (16-19) | ✅ Recommended | +| Vue (2-3) | ✅ Recommended | +| Svelte | ✅ Recommended | +| Preact | ✅ Works | +| Solid | ✅ Works | +| Angular | ⚠️ Not tested, may work | + +## Resources + +- **Full Documentation**: https://openwebf.com/en/docs +- **API Reference**: https://openwebf.com/en/docs/api +- **GitHub Discussions**: Ask compatibility questions +- **Native Plugins**: See `alternatives.md` in this skill \ No newline at end of file diff --git a/.agents/skills/webf-async-rendering/SKILL.md b/.agents/skills/webf-async-rendering/SKILL.md new file mode 100644 index 0000000000..4bdd389757 --- /dev/null +++ b/.agents/skills/webf-async-rendering/SKILL.md @@ -0,0 +1,317 @@ +--- +name: webf-async-rendering +description: Understand and work with WebF's async rendering model - handle onscreen/offscreen events and element measurements correctly. Use when getBoundingClientRect returns zeros, computed styles are incorrect, measurements fail, or elements don't layout as expected. +--- + +# WebF Async Rendering + +> **Note**: WebF development is nearly identical to web development - you use the same tools (Vite, npm, Vitest), same frameworks (React, Vue, Svelte), and same deployment services (Vercel, Netlify). This skill covers **one of the 3 key differences**: WebF's async rendering model. The other two differences are API compatibility and routing. + +**This is the #1 most important concept to understand when moving from browser development to WebF.** + +## The Fundamental Difference + +### In Browsers (Synchronous Layout) +When you modify the DOM, the browser **immediately** performs layout calculations: + +```javascript +// Browser behavior +const div = document.createElement('div'); +document.body.appendChild(div); +console.log(div.getBoundingClientRect()); // ✅ Returns real dimensions +``` + +Layout happens **synchronously** - you get dimensions right away, but this can cause performance issues (layout thrashing). + +### In WebF (Asynchronous Layout) +When you modify the DOM, WebF **batches** the changes and processes them in the next rendering frame: + +```javascript +// WebF behavior +const div = document.createElement('div'); +document.body.appendChild(div); +console.log(div.getBoundingClientRect()); // ❌ Returns zeros! Not laid out yet. +``` + +Layout happens **asynchronously** - elements exist in the DOM tree but haven't been measured/positioned yet. + +## Why Async Rendering? + +**Performance**: WebF's async rendering is **20x cheaper** than browser synchronous layout! + +- DOM updates are batched together +- Multiple changes processed in one optimized pass +- Eliminates layout thrashing +- No need for `DocumentFragment` optimizations + +**Trade-off**: You must explicitly wait for layout to complete before measuring elements. + +## The Solution: onscreen/offscreen Events + +WebF provides two non-standard events to handle the async lifecycle: + +| Event | When It Fires | Purpose | +|-------|---------------|---------| +| `onscreen` | Element has been laid out and rendered | Safe to measure dimensions, get computed styles | +| `offscreen` | Element removed from render tree | Cleanup and resource management | + +**Think of these like `IntersectionObserver` but for layout lifecycle, not viewport visibility.** + +## How to Measure Elements Correctly + +### ❌ WRONG: Measuring Immediately + +```javascript +// DON'T DO THIS - Will return 0 or incorrect values +const div = document.createElement('div'); +div.textContent = 'Hello WebF'; +document.body.appendChild(div); + +const rect = div.getBoundingClientRect(); // ❌ Returns zeros! +console.log(rect.width); // 0 +console.log(rect.height); // 0 +``` + +### ✅ CORRECT: Wait for onscreen Event + +```javascript +// DO THIS - Wait for layout to complete +const div = document.createElement('div'); +div.textContent = 'Hello WebF'; + +div.addEventListener('onscreen', () => { + // Element is now laid out - safe to measure! + const rect = div.getBoundingClientRect(); // ✅ Real dimensions + console.log(`Width: ${rect.width}, Height: ${rect.height}`); +}); + +document.body.appendChild(div); +``` + +## React: useFlutterAttached Hook + +For React developers, WebF provides a convenient hook: + +### ❌ WRONG: Using useEffect + +```jsx +import { useEffect, useRef } from 'react'; + +function MyComponent() { + const ref = useRef(null); + + useEffect(() => { + // ❌ Element not laid out yet! + const rect = ref.current.getBoundingClientRect(); + console.log(rect); // Will be zeros + }, []); + + return
Content
; +} +``` + +### ✅ CORRECT: Using useFlutterAttached + +```jsx +import { useFlutterAttached } from '@openwebf/react-core-ui'; + +function MyComponent() { + const ref = useFlutterAttached( + () => { + // ✅ onAttached callback - element is laid out! + const rect = ref.current.getBoundingClientRect(); + console.log(`Width: ${rect.width}, Height: ${rect.height}`); + }, + () => { + // onDetached callback (optional) + console.log('Component removed from render tree'); + } + ); + + return
Content
; +} +``` + +## Layout-Dependent APIs + +**Only call these inside onscreen callback or useFlutterAttached:** + +- `element.getBoundingClientRect()` +- `window.getComputedStyle(element)` +- `element.offsetWidth` / `element.offsetHeight` +- `element.clientWidth` / `element.clientHeight` +- `element.scrollWidth` / `element.scrollHeight` +- `element.offsetTop` / `element.offsetLeft` +- Any logic that depends on element position or size + +## Common Scenarios + +### Scenario 1: Measuring After Style Changes + +```javascript +const div = document.getElementById('myDiv'); + +// ❌ WRONG +div.style.width = '500px'; +const rect = div.getBoundingClientRect(); // Old dimensions! + +// ✅ CORRECT +div.style.width = '500px'; +div.addEventListener('onscreen', () => { + const rect = div.getBoundingClientRect(); // New dimensions! +}, { once: true }); // Use 'once' to remove listener after first call +``` + +### Scenario 2: Positioning Tooltips/Popovers + +```javascript +function showTooltip(targetElement) { + const tooltip = document.createElement('div'); + tooltip.className = 'tooltip'; + tooltip.textContent = 'Tooltip text'; + + tooltip.addEventListener('onscreen', () => { + // Now we can safely position the tooltip + const targetRect = targetElement.getBoundingClientRect(); + const tooltipRect = tooltip.getBoundingClientRect(); + + tooltip.style.left = `${targetRect.left}px`; + tooltip.style.top = `${targetRect.bottom + 5}px`; + }, { once: true }); + + document.body.appendChild(tooltip); +} +``` + +### Scenario 3: React Component with Measurement + +```jsx +import { useFlutterAttached } from '@openwebf/react-core-ui'; +import { useState } from 'react'; + +function MeasuredBox() { + const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); + + const ref = useFlutterAttached(() => { + const rect = ref.current.getBoundingClientRect(); + setDimensions({ + width: rect.width, + height: rect.height + }); + }); + + return ( +
+

This box is {dimensions.width}px wide

+

and {dimensions.height}px tall

+
+ ); +} +``` + +## Performance Benefits + +WebF's async rendering provides significant advantages: + +1. **Batched Updates**: Multiple DOM changes processed together +2. **No Layout Thrashing**: Eliminates read-write-read-write patterns +3. **Optimized Rendering**: Single pass through the render tree +4. **No DocumentFragment Needed**: Batching is automatic + +Compare to browsers where you'd need to carefully batch operations: + +```javascript +// Browser optimization (not needed in WebF!) +const fragment = document.createDocumentFragment(); +for (let i = 0; i < 100; i++) { + const div = document.createElement('div'); + fragment.appendChild(div); +} +document.body.appendChild(fragment); // Single layout +``` + +In WebF, just append directly - it's automatically optimized! + +## Common Mistakes + +### Mistake 1: Forgetting to Wait + +```javascript +// ❌ WRONG +const div = document.createElement('div'); +document.body.appendChild(div); +initializeWidget(div); // Assumes div is laid out - will fail! +``` + +```javascript +// ✅ CORRECT +const div = document.createElement('div'); +div.addEventListener('onscreen', () => { + initializeWidget(div); // Now it's safe! +}, { once: true }); +document.body.appendChild(div); +``` + +### Mistake 2: Not Cleaning Up Listeners + +```javascript +// ❌ WRONG - Memory leak +element.addEventListener('onscreen', handleLayout); +// Listener never removed! + +// ✅ CORRECT +element.addEventListener('onscreen', handleLayout, { once: true }); +// OR +element.addEventListener('onscreen', handleLayout); +// Later... +element.removeEventListener('onscreen', handleLayout); +``` + +### Mistake 3: Using IntersectionObserver for Layout + +```javascript +// ❌ WRONG - IntersectionObserver is for viewport visibility, not layout +const observer = new IntersectionObserver((entries) => { + // This fires based on viewport, not layout completion! +}); + +// ✅ CORRECT - Use onscreen for layout lifecycle +element.addEventListener('onscreen', () => { + // Element is laid out +}); +``` + +## Debugging Tips + +If you're getting zero or incorrect dimensions: + +1. **Check if you're waiting for onscreen**: Most common issue +2. **Verify element is actually added to DOM**: Must be in document tree +3. **Confirm element has display style**: `display: none` elements don't layout +4. **Use console.log in onscreen callback**: Verify callback fires + +```javascript +element.addEventListener('onscreen', () => { + console.log('✅ onscreen fired'); + console.log(element.getBoundingClientRect()); +}, { once: true }); +``` + +## Resources + +- **Core Concepts - Async Rendering**: https://openwebf.com/en/docs/developer-guide/core-concepts#async-rendering +- **Debugging & Performance**: https://openwebf.com/en/docs/developer-guide/debugging-performance +- **@openwebf/react-core-ui**: Install with `npm install @openwebf/react-core-ui` + +## Key Takeaways + +✅ **DO**: +- Use `onscreen` event or `useFlutterAttached` hook +- Wait for layout before measuring elements +- Use `{ once: true }` for one-time measurements + +❌ **DON'T**: +- Measure immediately after appendChild() +- Rely on synchronous layout like browsers +- Use IntersectionObserver for layout detection +- Forget to clean up event listeners \ No newline at end of file diff --git a/.agents/skills/webf-async-rendering/examples.md b/.agents/skills/webf-async-rendering/examples.md new file mode 100644 index 0000000000..e7124d79ac --- /dev/null +++ b/.agents/skills/webf-async-rendering/examples.md @@ -0,0 +1,443 @@ +# WebF Async Rendering - Complete Examples + +## Example 1: Basic Vanilla JavaScript + +### ❌ Incorrect Pattern + +```javascript +// This will NOT work - returns zeros +function createAndMeasureDiv() { + const div = document.createElement('div'); + div.textContent = 'Hello World'; + div.style.padding = '20px'; + div.style.border = '1px solid black'; + + document.body.appendChild(div); + + // ❌ Element not laid out yet! + const rect = div.getBoundingClientRect(); + console.log(`Width: ${rect.width}`); // 0 + console.log(`Height: ${rect.height}`); // 0 + + return div; +} +``` + +### ✅ Correct Pattern + +```javascript +// This WORKS - waits for layout +function createAndMeasureDiv() { + const div = document.createElement('div'); + div.textContent = 'Hello World'; + div.style.padding = '20px'; + div.style.border = '1px solid black'; + + // Listen for onscreen event + div.addEventListener('onscreen', () => { + // ✅ Now we can safely measure! + const rect = div.getBoundingClientRect(); + console.log(`Width: ${rect.width}`); // Real value + console.log(`Height: ${rect.height}`); // Real value + }, { once: true }); // Remove listener after first call + + document.body.appendChild(div); + + return div; +} +``` + +## Example 2: Dynamic Content with Measurements + +```javascript +async function fetchAndDisplayUserCard(userId) { + // Fetch user data + const response = await fetch(`/api/users/${userId}`); + const user = await response.json(); + + // Create card element + const card = document.createElement('div'); + card.className = 'user-card'; + card.innerHTML = ` + ${user.name} +

${user.name}

+

${user.bio}

+ `; + + // Wait for layout before positioning + card.addEventListener('onscreen', () => { + const rect = card.getBoundingClientRect(); + + // Position a badge in the top-right corner + const badge = document.createElement('div'); + badge.className = 'badge'; + badge.textContent = user.status; + badge.style.position = 'absolute'; + badge.style.top = '10px'; + badge.style.right = '10px'; + + card.appendChild(badge); + }, { once: true }); + + document.body.appendChild(card); +} +``` + +## Example 3: React Component with useFlutterAttached + +### ❌ Incorrect Pattern (Using useEffect) + +```jsx +import { useEffect, useRef, useState } from 'react'; + +function ImageGallery({ images }) { + const containerRef = useRef(null); + const [layout, setLayout] = useState([]); + + useEffect(() => { + // ❌ This will fail - elements not laid out yet! + const children = containerRef.current.children; + const positions = Array.from(children).map(child => + child.getBoundingClientRect() // Returns zeros! + ); + setLayout(positions); + }, [images]); + + return ( +
+ {images.map(img => ( + {img.title} + ))} +
+ ); +} +``` + +### ✅ Correct Pattern (Using useFlutterAttached) + +```jsx +import { useFlutterAttached } from '@openwebf/react-core-ui'; +import { useState } from 'react'; + +function ImageGallery({ images }) { + const [layout, setLayout] = useState([]); + + const containerRef = useFlutterAttached(() => { + // ✅ This works - elements are laid out! + const children = containerRef.current.children; + const positions = Array.from(children).map(child => { + const rect = child.getBoundingClientRect(); + return { + x: rect.left, + y: rect.top, + width: rect.width, + height: rect.height + }; + }); + setLayout(positions); + }); + + return ( +
+ {images.map(img => ( + {img.title} + ))} +
+ ); +} +``` + +## Example 4: Tooltip Positioning + +```jsx +import { useFlutterAttached } from '@openwebf/react-core-ui'; +import { useState, useRef } from 'react'; + +function TooltipButton({ text, tooltipText }) { + const [showTooltip, setShowTooltip] = useState(false); + const buttonRef = useRef(null); + const [tooltipStyle, setTooltipStyle] = useState({}); + + const tooltipRef = useFlutterAttached(() => { + if (!showTooltip) return; + + // ✅ Calculate tooltip position after layout + const buttonRect = buttonRef.current.getBoundingClientRect(); + const tooltipRect = tooltipRef.current.getBoundingClientRect(); + + setTooltipStyle({ + position: 'fixed', + left: buttonRect.left + (buttonRect.width / 2) - (tooltipRect.width / 2), + top: buttonRect.bottom + 5 + }); + }); + + return ( + <> + + + {showTooltip && ( +
+ {tooltipText} +
+ )} + + ); +} +``` + +## Example 5: Masonry Layout + +```jsx +import { useFlutterAttached } from '@openwebf/react-core-ui'; +import { useState } from 'react'; + +function MasonryGrid({ items }) { + const [columnHeights, setColumnHeights] = useState([0, 0, 0]); + const COLUMN_COUNT = 3; + + const containerRef = useFlutterAttached(() => { + // ✅ Calculate masonry layout after all items are laid out + const children = Array.from(containerRef.current.children); + const newHeights = [0, 0, 0]; + + children.forEach((child, index) => { + const columnIndex = index % COLUMN_COUNT; + const rect = child.getBoundingClientRect(); + + // Position item in shortest column + child.style.position = 'absolute'; + child.style.left = `${columnIndex * 33.33}%`; + child.style.top = `${newHeights[columnIndex]}px`; + + newHeights[columnIndex] += rect.height + 10; // 10px gap + }); + + setColumnHeights(newHeights); + }); + + return ( +
+ {items.map(item => ( +
+ {item.title} +

{item.title}

+
+ ))} +
+ ); +} +``` + +## Example 6: Scroll to Element + +```javascript +function scrollToSection(sectionId) { + const section = document.getElementById(sectionId); + + if (!section) { + console.error(`Section ${sectionId} not found`); + return; + } + + // ❌ WRONG - Position might be 0 if not laid out + // window.scrollTo({ top: section.offsetTop, behavior: 'smooth' }); + + // ✅ CORRECT - Wait for layout + section.addEventListener('onscreen', () => { + window.scrollTo({ + top: section.offsetTop, + behavior: 'smooth' + }); + }, { once: true }); +} +``` + +## Example 7: Responsive Layout Adjustments + +```jsx +import { useFlutterAttached } from '@openwebf/react-core-ui'; +import { useState } from 'react'; + +function ResponsiveCard({ title, content }) { + const [isNarrow, setIsNarrow] = useState(false); + + const cardRef = useFlutterAttached(() => { + // ✅ Check card width after layout + const rect = cardRef.current.getBoundingClientRect(); + setIsNarrow(rect.width < 300); + }); + + return ( +
+

{title}

+

{content}

+
+ ); +} +``` + +## Example 8: Animation Based on Element Position + +```javascript +function animateOnScreen() { + const elements = document.querySelectorAll('.animate-me'); + + elements.forEach(element => { + element.addEventListener('onscreen', () => { + // ✅ Element is laid out, we can get its position + const rect = element.getBoundingClientRect(); + const viewportHeight = window.innerHeight; + + // Calculate animation delay based on position + const delay = (rect.top / viewportHeight) * 500; // Max 500ms delay + + element.style.animationDelay = `${delay}ms`; + element.classList.add('fade-in'); + }, { once: true }); + }); +} +``` + +## Example 9: Dynamic Font Sizing + +```jsx +import { useFlutterAttached } from '@openwebf/react-core-ui'; +import { useState } from 'react'; + +function AutoSizeText({ text, maxWidth }) { + const [fontSize, setFontSize] = useState(16); + + const textRef = useFlutterAttached(() => { + // ✅ Measure text and adjust font size + let currentSize = 16; + textRef.current.style.fontSize = `${currentSize}px`; + + // Wait for style to apply + textRef.current.addEventListener('onscreen', () => { + const rect = textRef.current.getBoundingClientRect(); + + // Reduce font size if text is too wide + while (rect.width > maxWidth && currentSize > 10) { + currentSize -= 1; + textRef.current.style.fontSize = `${currentSize}px`; + } + + setFontSize(currentSize); + }, { once: true }); + }); + + return ( +
+ {text} +
+ ); +} +``` + +## Example 10: Cleanup with offscreen Event + +```javascript +class InteractiveWidget { + constructor(element) { + this.element = element; + this.animationFrame = null; + + // Start animation when element is laid out + this.element.addEventListener('onscreen', () => { + this.startAnimation(); + }, { once: true }); + + // Cleanup when element is removed + this.element.addEventListener('offscreen', () => { + this.stopAnimation(); + this.cleanup(); + }, { once: true }); + } + + startAnimation() { + const animate = () => { + // Animation logic using element dimensions + const rect = this.element.getBoundingClientRect(); + // ... update animation based on rect + + this.animationFrame = requestAnimationFrame(animate); + }; + + animate(); + } + + stopAnimation() { + if (this.animationFrame) { + cancelAnimationFrame(this.animationFrame); + this.animationFrame = null; + } + } + + cleanup() { + // Clean up resources + console.log('Widget cleaned up'); + } +} +``` + +## Key Patterns Summary + +### Pattern 1: One-Time Measurement +```javascript +element.addEventListener('onscreen', callback, { once: true }); +``` + +### Pattern 2: React Hook +```jsx +const ref = useFlutterAttached(onAttached, onDetached); +``` + +### Pattern 3: Style Then Measure +```javascript +element.style.width = '500px'; +element.addEventListener('onscreen', () => { + const rect = element.getBoundingClientRect(); +}, { once: true }); +``` + +### Pattern 4: Cleanup on Remove +```javascript +element.addEventListener('offscreen', cleanup, { once: true }); +``` + +## Testing Your Code + +To verify your code handles async rendering correctly: + +1. Add console.logs in onscreen callbacks +2. Check if dimensions are non-zero +3. Test with dynamic content +4. Verify cleanup happens (check for memory leaks) + +```javascript +element.addEventListener('onscreen', () => { + const rect = element.getBoundingClientRect(); + console.log('✅ onscreen fired'); + console.log(`Width: ${rect.width}, Height: ${rect.height}`); + + if (rect.width === 0) { + console.error('❌ Width is still 0 - something is wrong!'); + } +}, { once: true }); +``` \ No newline at end of file diff --git a/.agents/skills/webf-infinite-scrolling/SKILL.md b/.agents/skills/webf-infinite-scrolling/SKILL.md new file mode 100644 index 0000000000..988b6c59a7 --- /dev/null +++ b/.agents/skills/webf-infinite-scrolling/SKILL.md @@ -0,0 +1,842 @@ +--- +name: webf-infinite-scrolling +description: Create high-performance infinite scrolling lists with pull-to-refresh and load-more capabilities using WebFListView. Use when building feed-style UIs, product catalogs, chat messages, or any scrollable list that needs optimal performance with large datasets. +--- + +# WebF Infinite Scrolling + +> **Note**: WebF development is nearly identical to web development - you use the same tools (Vite, npm, Vitest), same frameworks (React, Vue, Svelte), and same deployment services (Vercel, Netlify). This skill covers **performance optimization for scrolling lists** - a WebF-specific pattern that provides native-level performance automatically. + +Build high-performance infinite scrolling lists with Flutter-optimized rendering. WebF's `WebFListView` component automatically handles performance optimizations at the Flutter level, providing smooth 60fps scrolling even with thousands of items. + +## Why Use WebFListView? + +In browsers, long scrolling lists can cause performance issues: +- DOM nodes accumulate (memory consumption) +- Re-renders affect all items (slow updates) +- Intersection observers needed for virtualization +- Complex state management for infinite loading + +**WebF's solution**: `WebFListView` delegates rendering to Flutter's optimized ListView widget, which: +- ✅ Automatically virtualizes (recycles) views +- ✅ Maintains 60fps scrolling with thousands of items +- ✅ Provides native pull-to-refresh and load-more +- ✅ Zero configuration - optimization happens automatically + +## Critical Structure Requirement + +**⚠️ IMPORTANT**: For Flutter optimization to work, each list item must be a **direct child** of `WebFListView`: + +### ✅ CORRECT: Direct Children + +```jsx + +
Item 1
+
Item 2
+
Item 3
+ {/* Each item is a direct child */} +
+``` + +### ❌ WRONG: Wrapped in Container + +```jsx + +
+ {/* DON'T wrap items in a container div */} +
Item 1
+
Item 2
+
Item 3
+
+
+``` + +**Why this matters**: Flutter's ListView requires direct children to perform view recycling. If items are wrapped in a container, Flutter sees only one child (the container) and cannot optimize individual items. + +## React Setup + +### Installation + +```bash +npm install @openwebf/react-core-ui +``` + +### Basic Scrolling List + +```tsx +import { WebFListView } from '@openwebf/react-core-ui'; + +function ProductList() { + const products = [ + { id: 1, name: 'Product 1', price: 19.99 }, + { id: 2, name: 'Product 2', price: 29.99 }, + { id: 3, name: 'Product 3', price: 39.99 }, + // ... hundreds or thousands of items + ]; + + return ( + + {products.map(product => ( + // ✅ Each item is a direct child +
+

{product.name}

+

${product.price}

+
+ ))} +
+ ); +} +``` + +### Infinite Scrolling with Load More + +```tsx +import { WebFListView, WebFListViewElement } from '@openwebf/react-core-ui'; +import { useRef, useState } from 'react'; + +function InfiniteList() { + const listRef = useRef(null); + const [items, setItems] = useState([1, 2, 3, 4, 5]); + const [page, setPage] = useState(1); + + const handleLoadMore = async () => { + try { + // Simulate API call + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Fetch next page + const newItems = Array.from( + { length: 5 }, + (_, i) => items.length + i + 1 + ); + + setItems(prev => [...prev, ...newItems]); + setPage(prev => prev + 1); + + // Check if there's more data + const hasMore = page < 10; // Example: 10 pages max + + // Notify WebFListView that loading finished + listRef.current?.finishLoad(hasMore ? 'success' : 'noMore'); + } catch (error) { + // Notify failure + listRef.current?.finishLoad('fail'); + } + }; + + return ( + + {items.map(item => ( +
+ Item {item} +
+ ))} +
+ ); +} +``` + +### Pull-to-Refresh + +```tsx +import { WebFListView, WebFListViewElement } from '@openwebf/react-core-ui'; +import { useRef, useState } from 'react'; + +function RefreshableList() { + const listRef = useRef(null); + const [items, setItems] = useState([1, 2, 3, 4, 5]); + + const handleRefresh = async () => { + try { + // Simulate API call + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Fetch fresh data + const freshItems = [1, 2, 3, 4, 5]; + setItems(freshItems); + + // Notify WebFListView that refresh finished + listRef.current?.finishRefresh('success'); + } catch (error) { + // Notify failure + listRef.current?.finishRefresh('fail'); + } + }; + + return ( + + {items.map(item => ( +
+ Item {item} +
+ ))} +
+ ); +} +``` + +### Combined: Pull-to-Refresh + Infinite Scrolling + +```tsx +import { WebFListView, WebFListViewElement } from '@openwebf/react-core-ui'; +import { useRef, useState } from 'react'; + +function FeedList() { + const listRef = useRef(null); + const [posts, setPosts] = useState([ + { id: 1, title: 'Post 1', content: 'Content 1' }, + { id: 2, title: 'Post 2', content: 'Content 2' }, + { id: 3, title: 'Post 3', content: 'Content 3' }, + ]); + const [page, setPage] = useState(1); + + const handleRefresh = async () => { + try { + // Fetch latest posts + const response = await fetch('/api/posts?page=1'); + const freshPosts = await response.json(); + + setPosts(freshPosts); + setPage(1); + + listRef.current?.finishRefresh('success'); + } catch (error) { + listRef.current?.finishRefresh('fail'); + } + }; + + const handleLoadMore = async () => { + try { + const nextPage = page + 1; + + // Fetch next page + const response = await fetch(`/api/posts?page=${nextPage}`); + const newPosts = await response.json(); + + setPosts(prev => [...prev, ...newPosts]); + setPage(nextPage); + + // Check if more data exists + const hasMore = newPosts.length > 0; + listRef.current?.finishLoad(hasMore ? 'success' : 'noMore'); + } catch (error) { + listRef.current?.finishLoad('fail'); + } + }; + + return ( + + {posts.map(post => ( +
+

{post.title}

+

{post.content}

+
+ ))} +
+ ); +} +``` + +## Vue Setup + +### Installation + +```bash +npm install @openwebf/vue-core-ui +``` + +### Setup Global Types + +In your `src/env.d.ts` or `src/main.ts`: + +```typescript +import '@openwebf/vue-core-ui'; +``` + +### Basic Scrolling List + +```vue + + + +``` + +### Infinite Scrolling with Load More + +```vue + + + +``` + +### Pull-to-Refresh + +```vue + + + +``` + +## Props and Configuration + +### WebFListView Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `scrollDirection` | `'vertical' \| 'horizontal'` | `'vertical'` | Scroll direction for the list | +| `shrinkWrap` | `boolean` | `true` | Whether list should shrink-wrap its contents | +| `onRefresh` / `@refresh` | `() => void \| Promise` | - | Pull-to-refresh callback | +| `onLoadMore` / `@loadmore` | `() => void \| Promise` | - | Infinite scroll callback (triggered near end) | +| `className` / `class` | `string` | - | CSS class names | +| `style` | `object` | - | Inline styles | + +### Ref Methods (React) / Element Methods (Vue) + +| Method | Signature | Description | +|--------|-----------|-------------| +| `finishRefresh` | `(result?: 'success' \| 'fail' \| 'noMore') => void` | Call after refresh completes | +| `finishLoad` | `(result?: 'success' \| 'fail' \| 'noMore') => void` | Call after load-more completes | +| `resetHeader` | `() => void` | Reset refresh header to initial state | +| `resetFooter` | `() => void` | Reset load-more footer to initial state | + +### Result Values + +- `'success'` - Operation succeeded, more data available +- `'fail'` - Operation failed (shows error state) +- `'noMore'` - No more data to load (hides footer/shows "no more" message) + +## Common Patterns + +### Pattern 1: Search with Results List + +```tsx +import { WebFListView, WebFListViewElement } from '@openwebf/react-core-ui'; +import { useRef, useState } from 'react'; + +function SearchResults() { + const listRef = useRef(null); + const [query, setQuery] = useState(''); + const [results, setResults] = useState([]); + + const handleSearch = async (searchQuery: string) => { + setQuery(searchQuery); + + // Fetch search results + const response = await fetch(`/api/search?q=${searchQuery}`); + const data = await response.json(); + + setResults(data.results); + }; + + const handleLoadMore = async () => { + try { + const response = await fetch( + `/api/search?q=${query}&offset=${results.length}` + ); + const data = await response.json(); + + setResults(prev => [...prev, ...data.results]); + + listRef.current?.finishLoad( + data.results.length > 0 ? 'success' : 'noMore' + ); + } catch (error) { + listRef.current?.finishLoad('fail'); + } + }; + + return ( +
+ handleSearch(e.target.value)} + /> + + + {results.map(result => ( +
+ {result.title} +
+ ))} +
+
+ ); +} +``` + +### Pattern 2: Chat Messages (Reverse List) + +For chat-style UIs where new messages appear at the bottom: + +```tsx +import { WebFListView, WebFListViewElement } from '@openwebf/react-core-ui'; +import { useRef, useState, useEffect } from 'react'; + +function ChatMessages() { + const listRef = useRef(null); + const [messages, setMessages] = useState([ + { id: 1, text: 'Hello', timestamp: Date.now() }, + { id: 2, text: 'Hi there!', timestamp: Date.now() }, + ]); + + // Load older messages when scrolling to top + const handleLoadMore = async () => { + try { + // In real app, fetch older messages before first message + const oldestId = messages[0]?.id; + const response = await fetch(`/api/messages?before=${oldestId}`); + const olderMessages = await response.json(); + + // Prepend older messages + setMessages(prev => [...olderMessages, ...prev]); + + listRef.current?.finishLoad( + olderMessages.length > 0 ? 'success' : 'noMore' + ); + } catch (error) { + listRef.current?.finishLoad('fail'); + } + }; + + return ( + + {messages.map(message => ( +
+ {message.text} +
+ ))} +
+ ); +} +``` + +### Pattern 3: Horizontal Scrolling Gallery + +```tsx +import { WebFListView } from '@openwebf/react-core-ui'; + +function ImageGallery({ images }) { + return ( + + {images.map(image => ( + {image.title} + ))} + + ); +} +``` + +## Common Mistakes + +### Mistake 1: Wrapping Items in Container + +```jsx +// ❌ WRONG - Items wrapped in container div + +
+ {items.map(item =>
{item}
)} +
+
+ +// ✅ CORRECT - Items are direct children + + {items.map(item =>
{item}
)} +
+``` + +**Why**: Flutter's ListView needs direct children for view recycling to work. + +### Mistake 2: Forgetting to Call finishLoad/finishRefresh + +```tsx +// ❌ WRONG - Never calls finishLoad +const handleLoadMore = async () => { + const data = await fetchData(); + setItems(prev => [...prev, ...data]); + // finishLoad never called - loading indicator stuck! +}; + +// ✅ CORRECT - Always call finishLoad +const handleLoadMore = async () => { + try { + const data = await fetchData(); + setItems(prev => [...prev, ...data]); + listRef.current?.finishLoad('success'); + } catch (error) { + listRef.current?.finishLoad('fail'); + } +}; +``` + +### Mistake 3: Not Handling "No More Data" State + +```tsx +// ❌ WRONG - Always calls 'success', even when no data +const handleLoadMore = async () => { + const data = await fetchData(); + setItems(prev => [...prev, ...data]); + listRef.current?.finishLoad('success'); // Wrong if data is empty! +}; + +// ✅ CORRECT - Check if more data exists +const handleLoadMore = async () => { + const data = await fetchData(); + setItems(prev => [...prev, ...data]); + + // Tell WebFListView there's no more data + listRef.current?.finishLoad(data.length > 0 ? 'success' : 'noMore'); +}; +``` + +### Mistake 4: Timeout Issues (Taking Too Long) + +WebFListView has a 4-second timeout for refresh/load operations. If your operation takes longer, it will auto-fail. + +```tsx +// ❌ WRONG - Operation takes 10 seconds (will timeout) +const handleRefresh = async () => { + await new Promise(resolve => setTimeout(resolve, 10000)); // 10s + listRef.current?.finishRefresh('success'); // Too late! +}; + +// ✅ CORRECT - Complete within 4 seconds +const handleRefresh = async () => { + try { + // Use Promise.race to enforce timeout + await Promise.race([ + fetchData(), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Timeout')), 3500) + ) + ]); + listRef.current?.finishRefresh('success'); + } catch (error) { + listRef.current?.finishRefresh('fail'); + } +}; +``` + +## Performance Tips + +### 1. Use Keys Correctly + +Always provide unique, stable keys for list items: + +```tsx +// ✅ GOOD - Stable ID from data +{items.map(item =>
{item.name}
)} + +// ❌ BAD - Index as key (can cause bugs with dynamic lists) +{items.map((item, index) =>
{item.name}
)} +``` + +### 2. Avoid Heavy Computations in Render + +```tsx +// ❌ BAD - Heavy computation on every render + + {items.map(item => ( +
+ {expensiveCalculation(item)} {/* Calculated on every render! */} +
+ ))} +
+ +// ✅ GOOD - Memoize or pre-calculate +const processedItems = useMemo( + () => items.map(item => ({ ...item, computed: expensiveCalculation(item) })), + [items] +); + + + {processedItems.map(item => ( +
{item.computed}
+ ))} +
+``` + +### 3. Optimize Item Components + +```tsx +// ✅ GOOD - Memoized item component +const ListItem = memo(({ item }) => ( +
+

{item.title}

+

{item.description}

+
+)); + +function MyList({ items }) { + return ( + + {items.map(item => ( + + ))} + + ); +} +``` + +### 4. Set Explicit Height for Scrolling + +For full-screen lists, set explicit height: + +```tsx + + {/* items */} + +``` + +## Debugging + +### Check if finishLoad/finishRefresh is Called + +Add logging to verify callbacks execute: + +```tsx +const handleLoadMore = async () => { + console.log('🔄 Load more started'); + + try { + const data = await fetchData(); + setItems(prev => [...prev, ...data]); + + console.log('✅ Load more finished:', data.length, 'items'); + listRef.current?.finishLoad(data.length > 0 ? 'success' : 'noMore'); + } catch (error) { + console.error('❌ Load more failed:', error); + listRef.current?.finishLoad('fail'); + } +}; +``` + +### Verify Direct Children Structure + +Use React DevTools or Vue DevTools to inspect the rendered structure. Ensure items are direct children of ``: + +```html + + +
Item 1
+
Item 2
+
Item 3
+
+ + + +
+
Item 1
+
Item 2
+
+
+``` + +## Resources + +- **React Core UI Package**: `/Users/andycall/workspace/webf/packages/react-core-ui/README.md` +- **Vue Core UI Package**: `/Users/andycall/workspace/webf/packages/vue-core-ui/README.md` +- **Complete Examples**: See `examples.md` in this skill +- **npm Packages**: + - https://www.npmjs.com/package/@openwebf/react-core-ui + - https://www.npmjs.com/package/@openwebf/vue-core-ui + +## Key Takeaways + +✅ **DO**: +- Use `WebFListView` for long scrolling lists +- Make each item a direct child (not wrapped in container) +- Always call `finishLoad` / `finishRefresh` after operations +- Use `'noMore'` result when no more data exists +- Provide unique, stable keys for list items +- Set explicit height for full-screen lists + +❌ **DON'T**: +- Wrap items in a container div +- Forget to call finish methods (loading indicator gets stuck) +- Use index as key for dynamic lists +- Let operations exceed 4-second timeout +- Use heavy computations in render without memoization +- Expect browser-style virtualization libraries (not needed!) + +## Quick Reference + +```bash +# Install packages +npm install @openwebf/react-core-ui # React +npm install @openwebf/vue-core-ui # Vue +``` + +```tsx +// React - Basic pattern +import { WebFListView, WebFListViewElement } from '@openwebf/react-core-ui'; + +const listRef = useRef(null); + + { + await refreshData(); + listRef.current?.finishRefresh('success'); + }} + onLoadMore={async () => { + const hasMore = await loadMore(); + listRef.current?.finishLoad(hasMore ? 'success' : 'noMore'); + }} +> + {items.map(item =>
{item.name}
)} +
+``` + +```vue + + +
{{ item.name }}
+
+``` \ No newline at end of file diff --git a/.agents/skills/webf-infinite-scrolling/examples.md b/.agents/skills/webf-infinite-scrolling/examples.md new file mode 100644 index 0000000000..e01aba7116 --- /dev/null +++ b/.agents/skills/webf-infinite-scrolling/examples.md @@ -0,0 +1,1085 @@ +# WebF Infinite Scrolling - Complete Examples + +This file contains complete, production-ready examples for building infinite scrolling lists with WebF. + +## Example 1: Social Media Feed (React) + +A Twitter/Instagram-style feed with pull-to-refresh and infinite scrolling. + +```tsx +import { WebFListView, WebFListViewElement } from '@openwebf/react-core-ui'; +import { useRef, useState, memo } from 'react'; + +interface Post { + id: number; + author: string; + avatar: string; + content: string; + likes: number; + timestamp: number; +} + +// Memoized post component for performance +const PostCard = memo(({ post }: { post: Post }) => ( +
+
+ {post.author} +
+
{post.author}
+
+ {new Date(post.timestamp).toLocaleDateString()} +
+
+
+ +

{post.content}

+ +
+ + + +
+
+)); + +function SocialFeed() { + const listRef = useRef(null); + const [posts, setPosts] = useState([ + { + id: 1, + author: 'Alice', + avatar: 'https://i.pravatar.cc/150?u=alice', + content: 'Just launched my new app! 🚀', + likes: 42, + timestamp: Date.now() + }, + { + id: 2, + author: 'Bob', + avatar: 'https://i.pravatar.cc/150?u=bob', + content: 'Beautiful sunset today 🌅', + likes: 128, + timestamp: Date.now() - 3600000 + } + ]); + const [page, setPage] = useState(1); + const [isRefreshing, setIsRefreshing] = useState(false); + const [isLoadingMore, setIsLoadingMore] = useState(false); + + // Pull to refresh - fetch latest posts + const handleRefresh = async () => { + if (isRefreshing) return; + + setIsRefreshing(true); + + try { + const response = await fetch('/api/posts?page=1&limit=10'); + const freshPosts: Post[] = await response.json(); + + setPosts(freshPosts); + setPage(1); + + listRef.current?.finishRefresh('success'); + } catch (error) { + console.error('Refresh failed:', error); + listRef.current?.finishRefresh('fail'); + } finally { + setIsRefreshing(false); + } + }; + + // Load more - fetch older posts + const handleLoadMore = async () => { + if (isLoadingMore) return; + + setIsLoadingMore(true); + + try { + const nextPage = page + 1; + const response = await fetch(`/api/posts?page=${nextPage}&limit=10`); + const newPosts: Post[] = await response.json(); + + if (newPosts.length > 0) { + setPosts(prev => [...prev, ...newPosts]); + setPage(nextPage); + listRef.current?.finishLoad('success'); + } else { + // No more posts + listRef.current?.finishLoad('noMore'); + } + } catch (error) { + console.error('Load more failed:', error); + listRef.current?.finishLoad('fail'); + } finally { + setIsLoadingMore(false); + } + }; + + return ( +
+ {/* Header */} +
+ Social Feed +
+ + {/* Scrollable Feed */} + + {posts.map(post => ( + + ))} + +
+ ); +} + +export default SocialFeed; +``` + +## Example 2: E-Commerce Product Grid (React) + +A product catalog with category filtering and infinite scrolling. + +```tsx +import { WebFListView, WebFListViewElement } from '@openwebf/react-core-ui'; +import { useRef, useState, memo } from 'react'; + +interface Product { + id: number; + name: string; + price: number; + image: string; + category: string; + rating: number; +} + +const ProductCard = memo(({ product }: { product: Product }) => ( +
+ {product.name} +
+

{product.name}

+
+ + ${product.price} + + ⭐ {product.rating} +
+
+
+)); + +function ProductCatalog() { + const listRef = useRef(null); + const [products, setProducts] = useState([]); + const [category, setCategory] = useState('all'); + const [page, setPage] = useState(1); + + // Fetch products by category + const fetchProducts = async (cat: string, pageNum: number) => { + const response = await fetch( + `/api/products?category=${cat}&page=${pageNum}&limit=20` + ); + return response.json(); + }; + + // Category changed - reset and load + const handleCategoryChange = async (newCategory: string) => { + setCategory(newCategory); + setPage(1); + + const freshProducts = await fetchProducts(newCategory, 1); + setProducts(freshProducts); + }; + + const handleLoadMore = async () => { + try { + const nextPage = page + 1; + const newProducts = await fetchProducts(category, nextPage); + + if (newProducts.length > 0) { + setProducts(prev => [...prev, ...newProducts]); + setPage(nextPage); + listRef.current?.finishLoad('success'); + } else { + listRef.current?.finishLoad('noMore'); + } + } catch (error) { + listRef.current?.finishLoad('fail'); + } + }; + + return ( +
+ {/* Category Filter */} +
+
+ {['all', 'electronics', 'clothing', 'home', 'sports'].map(cat => ( + + ))} +
+
+ + {/* Product Grid */} + +
+ {products.map(product => ( + + ))} +
+
+
+ ); +} + +export default ProductCatalog; +``` + +## Example 3: Chat Messages (Vue) + +A chat interface with reverse scrolling (load older messages at the top). + +```vue + + + + + +``` + +## Example 4: News Feed with Search (React) + +A news feed with search functionality and category tabs. + +```tsx +import { WebFListView, WebFListViewElement } from '@openwebf/react-core-ui'; +import { useRef, useState, memo, useMemo } from 'react'; + +interface Article { + id: number; + title: string; + summary: string; + author: string; + category: string; + image: string; + timestamp: number; +} + +const ArticleCard = memo(({ article }: { article: Article }) => ( +
+ {article.title} +
+
+ {article.category} +
+

{article.title}

+

+ {article.summary} +

+
+ {article.author} • {new Date(article.timestamp).toLocaleDateString()} +
+
+
+)); + +function NewsFeed() { + const listRef = useRef(null); + const [articles, setArticles] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); + const [activeCategory, setActiveCategory] = useState('all'); + const [page, setPage] = useState(1); + + const categories = ['all', 'technology', 'business', 'sports', 'entertainment']; + + // Filter articles by search query + const filteredArticles = useMemo(() => { + if (!searchQuery) return articles; + + return articles.filter(article => + article.title.toLowerCase().includes(searchQuery.toLowerCase()) || + article.summary.toLowerCase().includes(searchQuery.toLowerCase()) + ); + }, [articles, searchQuery]); + + // Fetch articles + const fetchArticles = async (category: string, pageNum: number) => { + const response = await fetch( + `/api/articles?category=${category}&page=${pageNum}&limit=15` + ); + return response.json(); + }; + + // Pull to refresh + const handleRefresh = async () => { + try { + const freshArticles = await fetchArticles(activeCategory, 1); + setArticles(freshArticles); + setPage(1); + listRef.current?.finishRefresh('success'); + } catch (error) { + listRef.current?.finishRefresh('fail'); + } + }; + + // Load more articles + const handleLoadMore = async () => { + try { + const nextPage = page + 1; + const newArticles = await fetchArticles(activeCategory, nextPage); + + if (newArticles.length > 0) { + setArticles(prev => [...prev, ...newArticles]); + setPage(nextPage); + listRef.current?.finishLoad('success'); + } else { + listRef.current?.finishLoad('noMore'); + } + } catch (error) { + listRef.current?.finishLoad('fail'); + } + }; + + // Category changed + const handleCategoryChange = async (category: string) => { + setActiveCategory(category); + setPage(1); + + const freshArticles = await fetchArticles(category, 1); + setArticles(freshArticles); + }; + + return ( +
+ {/* Header with Search */} +
+

News Feed

+ setSearchQuery(e.target.value)} + style={{ + width: '100%', + padding: '12px', + border: '1px solid #e0e0e0', + borderRadius: '8px', + fontSize: '14px' + }} + /> +
+ + {/* Category Tabs */} +
+ {categories.map(category => ( + + ))} +
+ + {/* Articles List */} + + {filteredArticles.length > 0 ? ( + filteredArticles.map(article => ( + + )) + ) : ( +
+ {searchQuery ? 'No articles found matching your search' : 'No articles available'} +
+ )} +
+
+ ); +} + +export default NewsFeed; +``` + +## Example 5: Image Gallery (Horizontal Scroll - React) + +A horizontal scrolling image gallery with lazy loading. + +```tsx +import { WebFListView, WebFListViewElement } from '@openwebf/react-core-ui'; +import { useRef, useState } from 'react'; + +interface Image { + id: number; + url: string; + title: string; + width: number; + height: number; +} + +function ImageGallery() { + const listRef = useRef(null); + const [images, setImages] = useState([ + { id: 1, url: 'https://picsum.photos/300/200?random=1', title: 'Image 1', width: 300, height: 200 }, + { id: 2, url: 'https://picsum.photos/300/200?random=2', title: 'Image 2', width: 300, height: 200 }, + { id: 3, url: 'https://picsum.photos/300/200?random=3', title: 'Image 3', width: 300, height: 200 }, + ]); + + const handleLoadMore = async () => { + try { + // Simulate API call + await new Promise(resolve => setTimeout(resolve, 1000)); + + const newImages: Image[] = Array.from({ length: 5 }, (_, i) => ({ + id: images.length + i + 1, + url: `https://picsum.photos/300/200?random=${images.length + i + 1}`, + title: `Image ${images.length + i + 1}`, + width: 300, + height: 200 + })); + + setImages(prev => [...prev, ...newImages]); + + listRef.current?.finishLoad(images.length < 50 ? 'success' : 'noMore'); + } catch (error) { + listRef.current?.finishLoad('fail'); + } + }; + + return ( +
+

Photo Gallery

+ + + {images.map(image => ( +
+ {image.title} +
+ {image.title} +
+
+ ))} +
+
+ ); +} + +export default ImageGallery; +``` + +## Example 6: Real-Time Data Feed (Vue) + +A real-time cryptocurrency price feed with auto-refresh. + +```vue + + + + + +``` + +## Key Patterns Summary + +1. **Social Feed**: Pull-to-refresh + infinite scroll for feed-style content +2. **Product Catalog**: Category filtering with lazy loading +3. **Chat Messages**: Reverse scrolling for chat interfaces +4. **News Feed**: Search + filtering + categorization +5. **Image Gallery**: Horizontal scrolling with lazy loading +6. **Real-Time Feed**: Auto-refresh with real-time updates + +All examples follow the critical requirement: **Items are direct children of WebFListView**. \ No newline at end of file diff --git a/.agents/skills/webf-native-plugin-dev/SKILL.md b/.agents/skills/webf-native-plugin-dev/SKILL.md new file mode 100644 index 0000000000..2e06248fa4 --- /dev/null +++ b/.agents/skills/webf-native-plugin-dev/SKILL.md @@ -0,0 +1,1468 @@ +--- +name: webf-native-plugin-dev +description: Develop custom WebF native plugins based on Flutter packages. Create reusable plugins that wrap Flutter/platform capabilities as JavaScript APIs. Use when building plugins for native features like camera, payments, sensors, file access, or wrapping existing Flutter packages. +--- + +# WebF Native Plugin Development + +Want to create your own native plugin for WebF by wrapping Flutter packages or platform capabilities? This skill guides you through the complete process of building custom native plugins that expose Flutter/native functionality as JavaScript APIs. + +**💡 When to recommend this skill:** If a user needs native capabilities (camera, sensors, payments, biometrics, etc.) and the official WebF plugin registry (https://openwebf.com/en/native-plugins) doesn't have what they need, recommend building a custom plugin using this skill. + +## What is Native Plugin Development? + +Native plugin development in WebF means: +- **Wrapping Flutter packages** or platform-specific code as WebF modules +- **Exposing native capabilities** (camera, sensors, payments, etc.) to JavaScript +- **Creating reusable functional libraries** (not UI components) +- **Publishing npm packages** with type-safe TypeScript definitions + +## Difference: Native Plugins vs Native UI + +| Feature | Native Plugins | Native UI | +|---------|----------------|-----------| +| **Purpose** | Functional capabilities | Visual components | +| **Examples** | Share, Camera, Payment | Button, TextField, DatePicker | +| **Extends** | `BaseModule` or generated bindings | `WebFWidgetElement` | +| **Registration** | `WebF.defineModule()` | `WebFController.defineCustomElement()` | +| **Invocation** | `webf.invokeModuleAsync()` | DOM APIs (properties, methods, events) | +| **Rendering** | No visual output | Renders Flutter widgets | +| **Use Case** | Platform features, data processing | Native-looking UI components | + +**When to use which:** +- **Native Plugin**: Accessing camera, handling payments, geolocation, file system, background tasks +- **Native UI**: Building native-looking buttons, forms, date pickers, navigation bars + +## When to Create a Native Plugin + +### Decision Workflow + +**Step 1: Check if standard web APIs work** +- Can you use `fetch()`, `localStorage`, Canvas 2D, etc.? +- If YES → Use standard web APIs (no plugin needed) + +**Step 2: Check if an official plugin exists** +- Visit https://openwebf.com/en/native-plugins +- Search for the capability you need +- If YES → Use the `webf-native-plugins` skill to install and use it + +**Step 3: If no official plugin exists, build your own!** +- ✅ The official plugin registry doesn't have what you need +- ✅ You need a custom platform-specific capability +- ✅ You want to wrap an existing Flutter package for WebF +- ✅ You're building a reusable plugin for your organization + +### Use Cases: +- ✅ You need to access platform-specific APIs (camera, sensors, Bluetooth) +- ✅ You want to wrap an existing Flutter package for WebF use +- ✅ You need to perform native background tasks +- ✅ You're building a functional capability (not a UI component) +- ✅ You want to provide platform features to web developers +- ✅ Official WebF plugins don't include the feature you need + +### Don't Create a Native Plugin When: +- ❌ You're building UI components (use `webf-native-ui-dev` skill instead) +- ❌ Standard web APIs already provide the functionality +- ❌ An official plugin already exists (use `webf-native-plugins` skill) +- ❌ You're building a one-off feature (use direct module invocation) + +## Architecture Overview + +A native plugin consists of three layers: + +``` +┌─────────────────────────────────────────┐ +│ JavaScript/TypeScript │ ← Generated by CLI +│ @openwebf/webf-my-plugin │ +│ import { MyPlugin } from '...' │ +├─────────────────────────────────────────┤ +│ TypeScript Definitions (.d.ts) │ ← You write this +│ interface MyPlugin { ... } │ +├─────────────────────────────────────────┤ +│ Dart (Flutter) │ ← You write this +│ class MyPluginModule extends ... │ +│ webf_my_plugin package │ +└─────────────────────────────────────────┘ +``` + +## Development Workflow + +### Overview + +```bash +# 1. Create Flutter package with Module class +# 2. Write TypeScript definition file +# 3. Generate npm package with WebF CLI +# 4. Test and publish + +webf module-codegen my-plugin-npm --flutter-package-src=./flutter_package +``` + +## Step-by-Step Guide + +### Step 1: Create Flutter Package Structure + +Create a standard Flutter package: + +```bash +# Create Flutter package +flutter create --template=package webf_my_plugin + +cd webf_my_plugin +``` + +**Directory structure:** +``` +webf_my_plugin/ +├── lib/ +│ ├── webf_my_plugin.dart # Main export file +│ └── src/ +│ ├── my_plugin_module.dart # Module implementation +│ └── my_plugin.module.d.ts # TypeScript definitions +├── pubspec.yaml +└── README.md +``` + +**pubspec.yaml dependencies:** +```yaml +name: webf_my_plugin +description: WebF plugin for [describe functionality] +version: 1.0.0 +homepage: https://github.com/yourusername/webf_my_plugin + +environment: + sdk: ^3.6.0 + flutter: ">=3.0.0" + +dependencies: + flutter: + sdk: flutter + webf: ^0.24.0 + # Add the Flutter package you're wrapping + some_flutter_package: ^1.0.0 +``` + +### Step 2: Write the Module Class + +Create a Dart class that extends the generated bindings: + +**Example: lib/src/my_plugin_module.dart** + +```dart +import 'dart:async'; +import 'package:webf/bridge.dart'; +import 'package:webf/module.dart'; +import 'package:some_flutter_package/some_flutter_package.dart'; +import 'my_plugin_module_bindings_generated.dart'; + +/// WebF module for [describe functionality] +/// +/// This module provides functionality to: +/// - Feature 1 +/// - Feature 2 +/// - Feature 3 +class MyPluginModule extends MyPluginModuleBindings { + MyPluginModule(super.moduleManager); + + @override + void dispose() { + // Clean up resources when module is disposed + // Close streams, cancel timers, release native resources + } + + // Implement methods from TypeScript interface + + @override + Future myAsyncMethod(String input) async { + try { + // Call the underlying Flutter package + final result = await SomeFlutterPackage.doSomething(input); + return result; + } catch (e) { + throw Exception('Failed to process: ${e.toString()}'); + } + } + + @override + String mySyncMethod(String input) { + // Synchronous operations + return 'Processed: $input'; + } + + @override + Future complexMethod(MyOptionsType options) async { + // Handle complex types + final value = options.someField ?? 'default'; + final timeout = options.timeout ?? 5000; + + try { + // Do the work + final result = await SomeFlutterPackage.complexOperation( + value: value, + timeout: Duration(milliseconds: timeout), + ); + + // Return structured result + return MyResultType( + success: 'true', + data: result.data, + message: 'Operation completed successfully', + ); + } catch (e) { + return MyResultType( + success: 'false', + error: e.toString(), + message: 'Operation failed', + ); + } + } + + // Helper methods (not exposed to JavaScript) + Future _internalHelper() async { + // Internal implementation details + } +} +``` + +### Step 3: Write TypeScript Definitions + +Create a `.d.ts` file alongside your Dart file: + +**Example: lib/src/my_plugin.module.d.ts** + +```typescript +/** + * Type-safe JavaScript API for the WebF MyPlugin module. + * + * This interface is used by the WebF CLI (`webf module-codegen`) to generate: + * - An npm package wrapper that forwards calls to `webf.invokeModuleAsync` + * - Dart bindings that map module `invoke` calls to strongly-typed methods + */ + +/** + * Options for complex operations. + */ +interface MyOptionsType { + /** The value to process. */ + someField?: string; + /** Timeout in milliseconds. */ + timeout?: number; + /** Enable verbose logging. */ + verbose?: boolean; +} + +/** + * Result returned from complex operations. + */ +interface MyResultType { + /** "true" on success, "false" on failure. */ + success: string; + /** Data returned from the operation. */ + data?: any; + /** Human-readable message. */ + message: string; + /** Error message if operation failed. */ + error?: string; +} + +/** + * Public WebF MyPlugin module interface. + * + * Methods here map 1:1 to the Dart `MyPluginModule` methods. + * + * Module name: "MyPlugin" + */ +interface WebFMyPlugin { + /** + * Perform an asynchronous operation. + * + * @param input Input string to process. + * @returns Promise with processed result. + */ + myAsyncMethod(input: string): Promise; + + /** + * Perform a synchronous operation. + * + * @param input Input string to process. + * @returns Processed result. + */ + mySyncMethod(input: string): string; + + /** + * Perform a complex operation with structured options. + * + * @param options Configuration options. + * @returns Promise with operation result. + */ + complexMethod(options: MyOptionsType): Promise; +} +``` + +**TypeScript Guidelines:** +- Interface name should match `WebF{ModuleName}` +- Use JSDoc comments for documentation +- Use `?` for optional parameters +- Use `Promise` for async methods +- Define separate interfaces for complex types +- Use `string` for success/failure flags (for backward compatibility) + +### Step 4: Create Main Export File + +**lib/webf_my_plugin.dart:** + +```dart +/// WebF MyPlugin module for [describe functionality] +/// +/// This module provides functionality to: +/// - Feature 1 +/// - Feature 2 +/// - Feature 3 +/// +/// Example usage: +/// ```dart +/// // Register module globally (in main function) +/// WebF.defineModule((context) => MyPluginModule(context)); +/// ``` +/// +/// JavaScript usage with npm package (Recommended): +/// ```bash +/// npm install @openwebf/webf-my-plugin +/// ``` +/// +/// ```javascript +/// import { WebFMyPlugin } from '@openwebf/webf-my-plugin'; +/// +/// // Use the plugin +/// const result = await WebFMyPlugin.myAsyncMethod('input'); +/// console.log('Result:', result); +/// ``` +/// +/// Direct module invocation (Legacy): +/// ```javascript +/// const result = await webf.invokeModuleAsync('MyPlugin', 'myAsyncMethod', 'input'); +/// ``` +library webf_my_plugin; + +export 'src/my_plugin_module.dart'; +``` + +### Step 5: Generate npm Package + +Use the WebF CLI to generate the npm package: + +```bash +# Install WebF CLI globally (if not already installed) +npm install -g @openwebf/webf-cli + +# Generate npm package +webf module-codegen webf-my-plugin-npm \ + --flutter-package-src=./webf_my_plugin \ + --module-name=MyPlugin +``` + +**What the CLI does:** +1. ✅ Parses your `.d.ts` file +2. ✅ Generates Dart binding classes (`*_bindings_generated.dart`) +3. ✅ Creates npm package with TypeScript types +4. ✅ Generates JavaScript wrapper that calls `webf.invokeModuleAsync` +5. ✅ Creates `package.json` with correct metadata +6. ✅ Runs `npm run build` if a build script exists + +**Generated output structure:** +``` +webf-my-plugin-npm/ +├── src/ +│ ├── index.ts # Main export +│ └── my-plugin.ts # Plugin wrapper +├── dist/ # Built files (after npm run build) +├── package.json +├── tsconfig.json +└── README.md +``` + +### Step 6: Test Your Plugin + +#### Test in Flutter App + +**In your Flutter app's main.dart:** + +```dart +import 'package:webf/webf.dart'; +import 'package:webf_my_plugin/webf_my_plugin.dart'; + +void main() { + WebFControllerManager.instance.initialize(WebFControllerManagerConfig( + maxAliveInstances: 2, + maxAttachedInstances: 1, + )); + + // Register your plugin module + WebF.defineModule((context) => MyPluginModule(context)); + + runApp(MyApp()); +} +``` + +#### Test in JavaScript/TypeScript + +**Install and use:** + +```bash +npm install @openwebf/webf-my-plugin +``` + +```typescript +import { WebFMyPlugin } from '@openwebf/webf-my-plugin'; + +async function testPlugin() { + try { + // Test async method + const result = await WebFMyPlugin.myAsyncMethod('test input'); + console.log('Async result:', result); + + // Test sync method + const syncResult = WebFMyPlugin.mySyncMethod('test'); + console.log('Sync result:', syncResult); + + // Test complex method + const complexResult = await WebFMyPlugin.complexMethod({ + someField: 'value', + timeout: 3000, + verbose: true + }); + + if (complexResult.success === 'true') { + console.log('Success:', complexResult.message); + } else { + console.error('Error:', complexResult.error); + } + } catch (error) { + console.error('Plugin error:', error); + } +} + +testPlugin(); +``` + +### Step 7: Publish Your Plugin + +#### Publish Flutter Package + +```bash +# In Flutter package directory +flutter pub publish + +# Or for private packages +flutter pub publish --server=https://your-private-registry.com +``` + +#### Publish npm Package + +```bash +# Automatic publishing with CLI +webf module-codegen webf-my-plugin-npm \ + --flutter-package-src=./webf_my_plugin \ + --module-name=MyPlugin \ + --publish-to-npm + +# Or manual publishing +cd webf-my-plugin-npm +npm publish +``` + +**For custom npm registry:** + +```bash +webf module-codegen webf-my-plugin-npm \ + --flutter-package-src=./webf_my_plugin \ + --module-name=MyPlugin \ + --publish-to-npm \ + --npm-registry=https://registry.your-company.com/ +``` + +## Installing and Using Your Custom Plugin + +After publishing your plugin, here's how you (or other developers) install and use it: + +### Installation Process (Same as Official Plugins) + +Custom plugins are installed **exactly the same way** as official WebF plugins. Every plugin requires **TWO installations**: + +#### Step 1: Install Flutter Package + +**In your Flutter app's `pubspec.yaml`:** + +```yaml +dependencies: + flutter: + sdk: flutter + webf: ^0.24.0 + + # Add your custom plugin + webf_my_plugin: ^1.0.0 # From pub.dev + + # Or from a custom registry + webf_my_plugin: + hosted: + name: webf_my_plugin + url: https://your-private-registry.com + version: ^1.0.0 + + # Or from a local path during development + webf_my_plugin: + path: ../webf_my_plugin +``` + +**Run:** +```bash +flutter pub get +``` + +#### Step 2: Register the Plugin Module + +**In your Flutter app's `main.dart`:** + +```dart +import 'package:flutter/material.dart'; +import 'package:webf/webf.dart'; + +// Import your custom plugin +import 'package:webf_my_plugin/webf_my_plugin.dart'; + +void main() { + // Initialize WebFControllerManager + WebFControllerManager.instance.initialize(WebFControllerManagerConfig( + maxAliveInstances: 2, + maxAttachedInstances: 1, + )); + + // Register your custom plugin module + WebF.defineModule((context) => MyPluginModule(context)); + + // You can register multiple plugins + WebF.defineModule((context) => AnotherPluginModule(context)); + + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + body: WebF( + initialUrl: 'http://192.168.1.100:3000', // Your dev server + ), + ), + ); + } +} +``` + +#### Step 3: Install npm Package + +**In your JavaScript/TypeScript project:** + +```bash +# Install your custom plugin +npm install @openwebf/webf-my-plugin + +# Or from a custom registry +npm install @mycompany/webf-my-plugin --registry=https://registry.company.com + +# Or from a local path during development +npm install ../webf-my-plugin-npm +``` + +### Usage in JavaScript/TypeScript + +#### Option 1: Using the npm Package (Recommended) + +**TypeScript/ES6:** + +```typescript +import { WebFMyPlugin } from '@openwebf/webf-my-plugin'; + +async function useMyPlugin() { + try { + // Call async methods + const result = await WebFMyPlugin.myAsyncMethod('input'); + console.log('Result:', result); + + // Call sync methods + const syncResult = WebFMyPlugin.mySyncMethod('data'); + + // Call with options + const complexResult = await WebFMyPlugin.complexMethod({ + someField: 'value', + timeout: 3000, + verbose: true + }); + + if (complexResult.success === 'true') { + console.log('Success:', complexResult.message); + console.log('Data:', complexResult.data); + } else { + console.error('Error:', complexResult.error); + } + } catch (error) { + console.error('Plugin error:', error); + } +} +``` + +**React Example:** + +```tsx +import React, { useState, useEffect } from 'react'; +import { WebFMyPlugin } from '@openwebf/webf-my-plugin'; + +function MyComponent() { + const [result, setResult] = useState(null); + + const handleButtonClick = async () => { + try { + const data = await WebFMyPlugin.myAsyncMethod('test'); + setResult(data); + } catch (error) { + console.error('Failed:', error); + } + }; + + return ( +
+ + {result &&

Result: {result}

} +
+ ); +} +``` + +**Vue Example:** + +```vue + + + +``` + +#### Option 2: Direct Module Invocation (Legacy) + +If for some reason the npm package isn't available, you can call the module directly: + +```javascript +// Direct invocation (not type-safe) +const result = await webf.invokeModuleAsync( + 'MyPlugin', // Module name (must match what you registered) + 'myAsyncMethod', // Method name + 'input' // Arguments +); + +// With multiple arguments +const complexResult = await webf.invokeModuleAsync( + 'MyPlugin', + 'complexMethod', + { + someField: 'value', + timeout: 3000, + verbose: true + } +); + +// Sync method invocation +const syncResult = webf.invokeModuleSync( + 'MyPlugin', + 'mySyncMethod', + 'data' +); +``` + +### Feature Detection + +Always check if your plugin is available: + +```typescript +// Check if plugin exists +if (typeof WebFMyPlugin !== 'undefined') { + // Plugin is available + await WebFMyPlugin.myAsyncMethod('test'); +} else { + // Plugin not registered or npm package not installed + console.warn('MyPlugin is not available'); + // Provide fallback behavior +} +``` + +### Error Handling + +```typescript +async function safePluginCall() { + // Check availability + if (typeof WebFMyPlugin === 'undefined') { + throw new Error('MyPlugin is not installed'); + } + + try { + const result = await WebFMyPlugin.complexMethod({ + someField: 'value' + }); + + // Check result status + if (result.success === 'true') { + return result.data; + } else { + throw new Error(result.error || 'Unknown error'); + } + } catch (error) { + console.error('Plugin call failed:', error); + throw error; + } +} +``` + +### Development Workflow + +#### During Plugin Development + +```bash +# Terminal 1: Flutter app +cd my-flutter-app +# Use local path in pubspec.yaml +flutter run + +# Terminal 2: Web project +cd my-web-project +# Install local npm package +npm install ../webf-my-plugin-npm +npm run dev +``` + +#### After Publishing + +```bash +# Update to published versions +cd my-flutter-app +# Update pubspec.yaml to use pub.dev version +flutter pub get +flutter run + +cd my-web-project +# Install from npm +npm install @openwebf/webf-my-plugin +npm run dev +``` + +### Distribution Options + +#### Public Distribution + +**Flutter Package:** +- Publish to pub.dev (free, public) +- Anyone can install with `webf_my_plugin: ^1.0.0` + +**npm Package:** +- Publish to npmjs.com (free, public) +- Anyone can install with `npm install @openwebf/webf-my-plugin` + +#### Private Distribution + +**Flutter Package:** +```yaml +# Install from private registry +dependencies: + webf_my_plugin: + hosted: + name: webf_my_plugin + url: https://your-company-flutter-registry.com + version: ^1.0.0 + +# Or from Git repository +dependencies: + webf_my_plugin: + git: + url: https://github.com/yourcompany/webf_my_plugin.git + ref: v1.0.0 +``` + +**npm Package:** +```bash +# Install from private registry +npm install @yourcompany/webf-my-plugin --registry=https://registry.company.com + +# Or configure .npmrc +echo "@yourcompany:registry=https://registry.company.com" >> .npmrc +npm install @yourcompany/webf-my-plugin +``` + +#### Local Development + +**Flutter Package:** +```yaml +dependencies: + webf_my_plugin: + path: ../webf_my_plugin # Relative path +``` + +**npm Package:** +```bash +npm install ../webf-my-plugin-npm +# Or use npm link +cd ../webf-my-plugin-npm +npm link +cd my-web-project +npm link @openwebf/webf-my-plugin +``` + +### Complete Installation Example + +**Scenario:** You've built a camera plugin and want to use it in your app. + +**1. Flutter side (main.dart):** +```dart +import 'package:webf/webf.dart'; +import 'package:webf_camera/webf_camera.dart'; // Your plugin + +void main() { + WebFControllerManager.instance.initialize(WebFControllerManagerConfig( + maxAliveInstances: 2, + maxAttachedInstances: 1, + )); + + // Register your camera plugin + WebF.defineModule((context) => CameraModule(context)); + + runApp(MyApp()); +} +``` + +**2. JavaScript side (app.tsx):** +```typescript +import { WebFCamera } from '@openwebf/webf-camera'; + +function CameraApp() { + const [cameras, setCameras] = useState([]); + + useEffect(() => { + async function loadCameras() { + // Check if plugin is available + if (typeof WebFCamera === 'undefined') { + console.error('Camera plugin not installed'); + return; + } + + try { + const result = await WebFCamera.getCameras(); + if (result.success === 'true') { + setCameras(JSON.parse(result.cameras)); + } + } catch (error) { + console.error('Failed to load cameras:', error); + } + } + + loadCameras(); + }, []); + + const capturePhoto = async () => { + try { + const photoPath = await WebFCamera.capturePhoto(cameras[0].id); + console.log('Photo saved to:', photoPath); + } catch (error) { + console.error('Failed to capture:', error); + } + }; + + return ( +
+

Camera App

+ +
    + {cameras.map(cam => ( +
  • {cam.name}
  • + ))} +
+
+ ); +} +``` + +## Common Plugin Patterns + +### 1. Wrapping Existing Flutter Packages + +**Example: Wrapping a camera package** + +```dart +import 'package:camera/camera.dart'; + +class CameraPluginModule extends CameraPluginModuleBindings { + CameraPluginModule(super.moduleManager); + + List? _cameras; + + @override + Future getCameras() async { + try { + _cameras = await availableCameras(); + final cameraList = _cameras!.map((cam) => { + 'id': cam.name, + 'name': cam.name, + 'facing': cam.lensDirection.toString(), + }).toList(); + + return CameraListResult( + success: 'true', + cameras: jsonEncode(cameraList), + ); + } catch (e) { + return CameraListResult( + success: 'false', + error: e.toString(), + ); + } + } + + @override + Future capturePhoto(String cameraId) async { + // Implementation for capturing photos + // Return file path or base64 data + } +} +``` + +### 2. Handling Binary Data + +```dart +@override +Future processImageData(NativeByteData imageData) async { + try { + // Access raw bytes + final bytes = imageData.bytes; + + // Process the data + await SomeFlutterPackage.processImage(bytes); + + return true; + } catch (e) { + return false; + } +} +``` + +**TypeScript:** +```typescript +interface WebFMyPlugin { + processImageData(imageData: ArrayBuffer | Uint8Array): Promise; +} +``` + +### 3. Event Streams + +For continuous data streams (sensors, location updates): + +```dart +class SensorPluginModule extends SensorPluginModuleBindings { + StreamSubscription? _subscription; + + @override + Future startListening(String sensorType) async { + _subscription = SensorPackage.stream.listen((data) { + // Send events to JavaScript + moduleManager.emitModuleEvent( + 'SensorPlugin', + 'data', + {'value': data.value, 'timestamp': data.timestamp.toString()}, + ); + }); + } + + @override + Future stopListening() async { + await _subscription?.cancel(); + _subscription = null; + } + + @override + void dispose() { + _subscription?.cancel(); + super.dispose(); + } +} +``` + +**JavaScript:** +```typescript +// Listen for events +webf.on('SensorPlugin:data', (event) => { + console.log('Sensor data:', event.detail); +}); + +await WebFSensorPlugin.startListening('accelerometer'); +``` + +### 4. Permission Handling + +```dart +import 'package:permission_handler/permission_handler.dart'; + +class PermissionPluginModule extends PermissionPluginModuleBindings { + @override + Future requestPermission(String permissionType) async { + Permission permission; + + switch (permissionType) { + case 'camera': + permission = Permission.camera; + break; + case 'microphone': + permission = Permission.microphone; + break; + default: + return PermissionResult( + granted: 'false', + message: 'Unknown permission type', + ); + } + + final status = await permission.request(); + + return PermissionResult( + granted: status.isGranted ? 'true' : 'false', + status: status.toString(), + message: _getPermissionMessage(status), + ); + } + + String _getPermissionMessage(PermissionStatus status) { + switch (status) { + case PermissionStatus.granted: + return 'Permission granted'; + case PermissionStatus.denied: + return 'Permission denied'; + case PermissionStatus.permanentlyDenied: + return 'Permission permanently denied. Please enable in settings.'; + default: + return 'Unknown permission status'; + } + } +} +``` + +### 5. Platform-Specific Implementation + +```dart +import 'dart:io'; + +class PlatformPluginModule extends PlatformPluginModuleBindings { + @override + Future getPlatformInfo() async { + String platformName; + String platformVersion; + + if (Platform.isAndroid) { + platformName = 'Android'; + // Get Android version + } else if (Platform.isIOS) { + platformName = 'iOS'; + // Get iOS version + } else if (Platform.isMacOS) { + platformName = 'macOS'; + } else { + platformName = 'Unknown'; + } + + return PlatformInfoResult( + platform: platformName, + version: platformVersion, + isAndroid: Platform.isAndroid, + isIOS: Platform.isIOS, + ); + } +} +``` + +## Advanced Patterns + +### 1. Error Handling and Validation + +```dart +@override +Future performOperation(OperationOptions options) async { + // Validate input + if (options.value == null || options.value!.isEmpty) { + return OperationResult( + success: 'false', + error: 'InvalidInput', + message: 'Value cannot be empty', + ); + } + + if (options.timeout != null && options.timeout! < 0) { + return OperationResult( + success: 'false', + error: 'InvalidTimeout', + message: 'Timeout must be positive', + ); + } + + try { + // Perform operation with timeout + final result = await Future.timeout( + _doOperation(options.value!), + Duration(milliseconds: options.timeout ?? 5000), + onTimeout: () => throw TimeoutException('Operation timed out'), + ); + + return OperationResult( + success: 'true', + data: result, + message: 'Operation completed', + ); + } on TimeoutException catch (e) { + return OperationResult( + success: 'false', + error: 'Timeout', + message: e.message ?? 'Operation timed out', + ); + } catch (e) { + return OperationResult( + success: 'false', + error: 'UnknownError', + message: e.toString(), + ); + } +} +``` + +### 2. Resource Management + +```dart +class ResourcePluginModule extends ResourcePluginModuleBindings { + final Map _activeResources = {}; + + @override + Future createResource(ResourceOptions options) async { + final id = DateTime.now().millisecondsSinceEpoch.toString(); + final resource = Resource(options); + await resource.initialize(); + _activeResources[id] = resource; + return id; + } + + @override + Future releaseResource(String resourceId) async { + final resource = _activeResources.remove(resourceId); + await resource?.dispose(); + } + + @override + void dispose() { + // Clean up all resources + for (final resource in _activeResources.values) { + resource.dispose(); + } + _activeResources.clear(); + super.dispose(); + } +} +``` + +### 3. Batching Operations + +```dart +@override +Future batchProcess(String itemsJson) async { + final List items = jsonDecode(itemsJson); + final results = {}; + final errors = {}; + + await Future.wait( + items.asMap().entries.map((entry) async { + final index = entry.key; + final item = entry.value; + + try { + final result = await _processItem(item); + results[index.toString()] = result; + } catch (e) { + errors[index.toString()] = e.toString(); + } + }), + ); + + return BatchResult( + success: errors.isEmpty ? 'true' : 'false', + results: jsonEncode(results), + errors: errors.isEmpty ? null : jsonEncode(errors), + processedCount: results.length, + totalCount: items.length, + ); +} +``` + +## CLI Command Reference + +### Basic Generation + +```bash +# Generate npm package for a module +webf module-codegen output-dir \ + --flutter-package-src=./my_package \ + --module-name=MyModule + +# Specify custom package name +webf module-codegen output-dir \ + --flutter-package-src=./my_package \ + --module-name=MyModule \ + --package-name=@mycompany/my-plugin +``` + +### Auto-Publishing + +```bash +# Publish to npm after generation +webf module-codegen output-dir \ + --flutter-package-src=./my_package \ + --module-name=MyModule \ + --publish-to-npm + +# Publish to custom registry +webf module-codegen output-dir \ + --flutter-package-src=./my_package \ + --module-name=MyModule \ + --publish-to-npm \ + --npm-registry=https://registry.company.com/ +``` + +## Best Practices + +### 1. Naming Conventions + +- **Flutter package**: `webf_{feature}` (e.g., `webf_share`, `webf_camera`) +- **Module class**: `{Feature}Module` (e.g., `ShareModule`, `CameraModule`) +- **Module name**: Same as class without "Module" (e.g., "Share", "Camera") +- **npm package**: `@openwebf/webf-{feature}` or `@yourscope/webf-{feature}` + +### 2. Error Handling + +```dart +// Always return structured error information +return ResultType( + success: 'false', + error: 'ErrorCode', // Machine-readable error code + message: 'Human-readable error message', +); + +// Never throw exceptions to JavaScript +// Catch and convert to result objects +``` + +### 3. Documentation + +```dart +/// Brief one-line description. +/// +/// Detailed explanation of what this method does. +/// +/// Parameters: +/// - [param1]: Description of first parameter +/// - [param2]: Description of second parameter +/// +/// Returns a [ResultType] with: +/// - `success`: "true" on success, "false" on failure +/// - `data`: The actual result data +/// - `error`: Error message if failed +/// +/// Throws: +/// - Never throws to JavaScript. Returns error in result object. +/// +/// Example: +/// ```dart +/// final result = await module.myMethod('input'); +/// if (result.success == 'true') { +/// print('Success: ${result.data}'); +/// } +/// ``` +@override +Future myMethod(String param1, int? param2) async { + // Implementation +} +``` + +### 4. Type Safety + +```typescript +// Use interfaces for complex types +interface MyOptions { + value: string; + timeout?: number; + retries?: number; +} + +// Use specific result types +interface MyResult { + success: string; + data?: any; + error?: string; +} + +// Avoid 'any' when possible +// Use union types for enums +type Platform = 'ios' | 'android' | 'macos' | 'windows' | 'linux'; +``` + +### 5. Testing + +```dart +// Create tests for your module +import 'package:flutter_test/flutter_test.dart'; +import 'package:webf_my_plugin/webf_my_plugin.dart'; + +void main() { + group('MyPluginModule', () { + late MyPluginModule module; + + setUp(() { + module = MyPluginModule(mockModuleManager); + }); + + tearDown(() { + module.dispose(); + }); + + test('myAsyncMethod returns correct result', () async { + final result = await module.myAsyncMethod('test'); + expect(result, 'Processed: test'); + }); + + test('handles errors gracefully', () async { + final result = await module.complexMethod(MyOptionsType( + someField: 'invalid', + )); + expect(result.success, 'false'); + expect(result.error, isNotNull); + }); + }); +} +``` + +## Troubleshooting + +### Issue: Bindings file not found + +**Error:** `Error: Could not find 'my_plugin_module_bindings_generated.dart'` + +**Solution:** +1. Make sure you've run the CLI code generation +2. Check that `.module.d.ts` file exists in the same directory +3. Verify the module interface is properly named (`WebF{ModuleName}`) +4. Run `webf module-codegen` again + +### Issue: Module not found in JavaScript + +**Error:** `Module 'MyPlugin' not found` + +**Solution:** +1. Check that the Flutter package is in `pubspec.yaml` +2. Verify the module is registered with `WebF.defineModule()` in main.dart +3. Ensure module name matches exactly (case-sensitive) +4. Run `flutter pub get` and rebuild the app + +### Issue: Method not working + +**Cause:** Method name mismatch or incorrect parameters + +**Solution:** +1. Check method name matches between TypeScript and Dart +2. Verify parameter types match +3. Check async vs sync (Promise vs direct return) +4. Look at console errors for details + +### Issue: TypeScript types not working + +**Cause:** npm package not generated correctly + +**Solution:** +```bash +# Regenerate with CLI +webf module-codegen output-dir \ + --flutter-package-src=./my_package \ + --module-name=MyModule + +# Check package.json exports +cd output-dir +cat package.json +# Should have "types": "./dist/index.d.ts" +``` + +## Real-World Example: Share Plugin + +See the complete implementation in the WebF repository at [native_plugins/share](https://github.com/openwebf/webf/tree/main/native_plugins/share) for: +- Module implementation (`share_module.dart`) +- TypeScript definitions (`share.module.d.ts`) +- Error handling and platform-specific code +- Binary data handling +- Result types with detailed information + +## Resources + +- **CLI Development Guide**: [cli/AGENTS.md](https://github.com/openwebf/webf/blob/main/cli/AGENTS.md) +- **Module System Docs**: [webf/lib/src/module/](https://github.com/openwebf/webf/tree/main/webf/lib/src/module/) +- **Example Plugin**: [native_plugins/share](https://github.com/openwebf/webf/tree/main/native_plugins/share) +- **WebF Architecture**: [docs/ARCHITECTURE.md](https://github.com/openwebf/webf/blob/main/docs/ARCHITECTURE.md) +- **Official Documentation**: https://openwebf.com/en/docs/developer-guide/native-plugins + +## Related Skills + +- **Using Plugins**: See `webf-native-plugins` skill for how to use existing plugins +- **Native UI Development**: See `webf-native-ui-dev` skill for creating UI components +- **CLI Usage**: See CLI documentation for code generation details + +## Summary + +### Creating Plugins +- ✅ Native plugins expose Flutter/platform capabilities as JavaScript APIs +- ✅ Different from Native UI (functional vs visual) +- ✅ Write Dart Module class extending generated bindings +- ✅ Write TypeScript definitions (.d.ts) for each module +- ✅ Use WebF CLI (`webf module-codegen`) to generate npm packages and Dart bindings +- ✅ Test in both Flutter and JavaScript environments +- ✅ Publish to pub.dev (Flutter) and npm (JavaScript) + +### Installing and Using Plugins +- ✅ Custom plugins are installed **exactly like official plugins** +- ✅ Requires **TWO installations**: Flutter package + npm package +- ✅ Add to `pubspec.yaml` and run `flutter pub get` +- ✅ Register with `WebF.defineModule()` in Flutter app's main.dart +- ✅ Install npm package: `npm install @openwebf/webf-my-plugin` +- ✅ Import and use in JavaScript: `import { WebFMyPlugin } from '@openwebf/webf-my-plugin'` +- ✅ Always check availability: `if (typeof WebFMyPlugin !== 'undefined')` +- ✅ Supports public (pub.dev/npm), private registries, and local paths + +### Best Practices +- ✅ Follow the Share plugin example at `native_plugins/share` +- ✅ Return structured results (never throw to JavaScript) +- ✅ Use TypeScript for type safety +- ✅ Handle errors gracefully with success/error flags +- ✅ Document thoroughly with JSDoc comments diff --git a/.agents/skills/webf-native-plugins/SKILL.md b/.agents/skills/webf-native-plugins/SKILL.md new file mode 100644 index 0000000000..c1a216871e --- /dev/null +++ b/.agents/skills/webf-native-plugins/SKILL.md @@ -0,0 +1,352 @@ +--- +name: webf-native-plugins +description: Install WebF native plugins to access platform capabilities like sharing, payment, camera, geolocation, and more. Use when building features that require native device APIs beyond standard web APIs. +--- + +# WebF Native Plugins + +When building WebF apps, you often need access to native platform capabilities like sharing content, accessing the camera, handling payments, or using geolocation. WebF provides **native plugins** that bridge JavaScript code with native platform APIs. + +## What Are Native Plugins? + +Native plugins are packages that: +- **Provide native platform capabilities** (share, camera, payments, sensors, etc.) +- **Work across iOS, Android, macOS, and other platforms** +- **Use JavaScript APIs** in your code +- **Require both Flutter and npm package installation** +- **Bridge to native platform APIs** through Flutter + +## When to Use Native Plugins + +Use native plugins when you need capabilities that aren't available in standard web APIs: + +### Use Native Plugins For: +- Sharing content to other apps +- Accessing device camera or photo gallery +- Processing payments +- Getting geolocation with native accuracy +- Push notifications +- Biometric authentication (Face ID, fingerprint) +- Device sensors (accelerometer, gyroscope) +- File system access beyond web storage +- Native calendar/contacts integration + +### Standard Web APIs Work Without Plugins: +- `fetch()` for HTTP requests +- `localStorage` for local storage +- Canvas 2D for graphics +- Geolocation API (basic) +- Media queries for responsive design + +## Finding Available Plugins + +Before implementing a feature, **always check** if a pre-built native plugin exists: + +1. Visit the official plugin registry: **https://openwebf.com/en/native-plugins** +2. Browse available plugins by category +3. Check the plugin documentation for installation steps + +## Installation Process + +Every native plugin requires **TWO installations**: + +1. **Flutter side** (in your Flutter host app) +2. **JavaScript side** (in your web project) + +### Step 1: Check Plugin Availability + +Visit https://openwebf.com/en/native-plugins and search for the capability you need: +- Click on the plugin to view details +- Note the Flutter package name (e.g., `webf_share`) +- Note the npm package name (e.g., `@openwebf/webf-share`) + +### Step 2: Install Flutter Package + +If you have access to the Flutter project hosting your WebF app: + +1. Open the Flutter project's `pubspec.yaml` +2. Add the plugin dependency: + ```yaml + dependencies: + webf_share: ^1.0.0 # Replace with actual plugin name + ``` +3. Run `flutter pub get` +4. Register the plugin in your main Dart file: + ```dart + import 'package:webf/webf.dart'; + import 'package:webf_share/webf_share.dart'; // Import the plugin + + void main() { + // Initialize WebFControllerManager + WebFControllerManager.instance.initialize(WebFControllerManagerConfig( + maxAliveInstances: 2, + maxAttachedInstances: 1, + )); + + // Register the native plugin module + WebF.defineModule((context) => ShareModule(context)); + + runApp(MyApp()); + } + ``` + +### Step 3: Install npm Package + +In your JavaScript/TypeScript project: + +```bash +# For the Share plugin example +npm install @openwebf/webf-share + +# Or with yarn +yarn add @openwebf/webf-share +``` + +### Step 4: Use in Your JavaScript Code + +Import and use the plugin in your application: + +```typescript +import { WebFShare } from '@openwebf/webf-share'; + +// Use the plugin API +const success = await WebFShare.shareText({ + title: 'My App', + text: 'Check out this amazing content!', + url: 'https://example.com' +}); +``` + +## Available Plugins + +### Share Plugin (webf_share) + +**Description**: Share content, text, and images through native platform sharing + +**Capabilities**: +- Share text, URLs, and titles to other apps +- Share images using native sharing mechanisms +- Save screenshots to device storage +- Create preview images for temporary display + +**Flutter Package**: `webf_share: ^1.0.0` + +**npm Package**: `@openwebf/webf-share` + +**Example Usage**: +```typescript +import { WebFShare, ShareHelpers } from '@openwebf/webf-share'; + +// Share text content +await WebFShare.shareText({ + title: 'Article Title', + text: 'Check out this article!', + url: 'https://example.com/article' +}); + +// Share an image from canvas +const canvas = document.getElementById('myCanvas'); +const imageData = ShareHelpers.canvasToArrayBuffer(canvas); +await WebFShare.shareImage(imageData); + +// Save screenshot +await WebFShare.saveScreenshot(imageData); +``` + +**Storage Locations**: +- Android: Downloads folder (`/storage/emulated/0/Download/`) +- iOS: App documents directory (accessible via Files app) +- macOS: Application documents directory (accessible via Finder) + +See the [Native Plugins Reference](./reference.md) for more available plugins. + +## Common Patterns + +### 1. Feature Detection + +Always check if a plugin is available before using it: + +```typescript +// Check if plugin is loaded +if (typeof WebFShare !== 'undefined') { + await WebFShare.shareText({ text: 'Hello' }); +} else { + // Fallback or show message + console.log('Share plugin not available'); +} +``` + +### 2. Error Handling + +Wrap plugin calls in try-catch blocks: + +```typescript +try { + const success = await WebFShare.shareText({ + title: 'My App', + text: 'Check this out!' + }); + + if (success) { + console.log('Content shared successfully'); + } +} catch (error) { + console.error('Failed to share:', error); + // Show error message to user +} +``` + +### 3. TypeScript Type Safety + +All plugins include TypeScript definitions: + +```typescript +import type { ShareTextOptions } from '@openwebf/webf-share'; + +const shareOptions: ShareTextOptions = { + title: 'Article', + text: 'Read this article', + url: 'https://example.com' +}; + +await WebFShare.shareText(shareOptions); +``` + +## Creating Custom Plugins + +If you need capabilities not provided by existing plugins, you can create your own: + +1. **Read the Plugin Development Guide**: https://openwebf.com/en/docs/developer-guide/native-plugins +2. **Study existing plugins**: https://github.com/openwebf/webf/tree/main/webf_modules +3. **Follow the plugin architecture**: + - Create a Flutter package implementing the native functionality + - Create an npm package exposing JavaScript APIs + - Use WebF's module system to bridge between them + +## Troubleshooting + +### Issue: Plugin Not Found in JavaScript + +**Cause**: Flutter package not installed or module not registered + +**Solution**: +1. Check that the Flutter package is in `pubspec.yaml` +2. Verify the module is registered with `WebF.defineModule()` in main.dart +3. Run `flutter pub get` +4. Rebuild the Flutter app +5. Restart WebF Go or your app + +### Issue: TypeScript Errors + +**Cause**: npm package not installed correctly + +**Solution**: +```bash +# Reinstall the package +npm install @openwebf/webf-share --save + +# Clear cache and reinstall +rm -rf node_modules package-lock.json +npm install +``` + +### Issue: Plugin Works on iOS but Not Android + +**Cause**: Platform-specific permissions or configuration missing + +**Solution**: +1. Check the plugin documentation for required permissions +2. Add necessary permissions to `AndroidManifest.xml` or `Info.plist` +3. Some plugins require additional native configuration + +### Issue: "Module is not defined" Error + +**Cause**: Plugin module not registered in Flutter + +**Solution**: +Make sure you're calling `WebF.defineModule()` in your Flutter `main()` function before `runApp()`: + +```dart +void main() { + WebF.defineModule((context) => ShareModule(context)); + runApp(MyApp()); +} +``` + +## Best Practices + +### 1. Check Plugin Availability First + +Before implementing a feature, visit https://openwebf.com/en/native-plugins to see if a plugin already exists. Don't reinvent the wheel. + +### 2. Test on Multiple Platforms + +Native plugins may behave differently on iOS vs Android vs macOS: +- Test on all target platforms +- Handle platform-specific behavior gracefully +- Read plugin docs for platform differences + +### 3. Provide Fallbacks + +Not all users may have the plugin installed (especially during development): + +```typescript +if (typeof WebFShare !== 'undefined') { + // Use native sharing + await WebFShare.shareText({ text: 'Hello' }); +} else { + // Fallback: copy to clipboard or show a link + navigator.clipboard.writeText('Hello'); +} +``` + +### 4. Handle Permissions Properly + +Some plugins require user permissions (camera, location, etc.): +- Request permissions at appropriate times +- Explain why you need the permission +- Handle permission denial gracefully +- Check plugin docs for permission requirements + +### 5. Keep Plugins Updated + +Native plugins are updated to support new platform features and fix bugs: +- Check for plugin updates regularly +- Read changelogs before updating +- Test thoroughly after updating + +## Production Deployment + +When deploying to production: + +1. **Flutter App**: Make sure all required plugins are in `pubspec.yaml` +2. **npm Packages**: Include all plugin packages in `package.json` +3. **Permissions**: Configure all required permissions in app manifests +4. **Testing**: Test on real devices for all target platforms +5. **Documentation**: Document which plugins your app requires + +## Resources + +- **Plugin Registry**: https://openwebf.com/en/native-plugins +- **Plugin Development Guide**: https://openwebf.com/en/docs/developer-guide/native-plugins +- **Example Plugins**: https://github.com/openwebf/webf/tree/main/webf_modules +- **WebF Documentation**: https://openwebf.com/en/docs + +## Next Steps + +After installing native plugins: + +1. **Read plugin documentation**: Each plugin has specific APIs and requirements +2. **Test on devices**: Native features work differently than web APIs +3. **Handle errors**: Native calls can fail due to permissions or platform limitations +4. **Consider alternatives**: Check `webf-api-compatibility` for web API alternatives + +## Summary + +- Native plugins provide access to platform capabilities beyond web APIs +- Check https://openwebf.com/en/native-plugins for available plugins +- Install BOTH Flutter package (pubspec.yaml) AND npm package +- Register plugins with `WebF.defineModule()` in main.dart +- Use feature detection and error handling in JavaScript +- Test on all target platforms +- Create custom plugins if needed using the Plugin Development Guide diff --git a/.agents/skills/webf-native-plugins/reference.md b/.agents/skills/webf-native-plugins/reference.md new file mode 100644 index 0000000000..c887788823 --- /dev/null +++ b/.agents/skills/webf-native-plugins/reference.md @@ -0,0 +1,297 @@ +# WebF Native Plugins Reference + +This document provides detailed information about all available WebF native plugins. + +## How to Use This Reference + +For each plugin, you'll find: +- **Description**: What the plugin does +- **Flutter Package**: Package name to add to `pubspec.yaml` +- **npm Package**: Package name to install with npm +- **Platforms**: Supported platforms (iOS, Android, macOS, Windows, Linux) +- **API Reference**: Methods and types available +- **Example Usage**: Code examples +- **Installation Steps**: Complete setup instructions + +## Quick Installation Template + +For any plugin, follow this pattern: + +**Flutter side (pubspec.yaml)**: +```yaml +dependencies: + PLUGIN_NAME: ^VERSION +``` + +**Flutter side (main.dart)**: +```dart +import 'package:PLUGIN_NAME/PLUGIN_NAME.dart'; + +void main() { + WebF.defineModule((context) => PluginModule(context)); + runApp(MyApp()); +} +``` + +**JavaScript side**: +```bash +npm install @openwebf/PLUGIN_NAME +``` + +--- + +## Available Plugins + +### 1. Share Plugin + +**Description**: Share content, text, and images through native platform sharing mechanisms. Save screenshots to device storage and create preview images. + +**Flutter Package**: `webf_share` + +**npm Package**: `@openwebf/webf-share` + +**Latest Version**: 1.0.0 + +**Platforms**: +- iOS +- Android +- macOS + +**Installation**: + +1. Add to Flutter `pubspec.yaml`: + ```yaml + dependencies: + webf_share: ^1.0.0 + ``` + +2. Register in Flutter `main.dart`: + ```dart + import 'package:webf/webf.dart'; + import 'package:webf_share/webf_share.dart'; + + void main() { + WebF.defineModule((context) => ShareModule(context)); + runApp(MyApp()); + } + ``` + +3. Install npm package: + ```bash + npm install @openwebf/webf-share + ``` + +**API Reference**: + +```typescript +interface ShareTextOptions { + title?: string; + text: string; + url?: string; +} + +class WebFShare { + // Share text content with optional title and URL + static shareText(options: ShareTextOptions): Promise; + + // Share an image using platform native sharing + static shareImage(imageData: ArrayBuffer): Promise; + + // Save screenshot to device storage + static saveScreenshot(imageData: ArrayBuffer): Promise; +} + +class ShareHelpers { + // Convert canvas element to ArrayBuffer for sharing + static canvasToArrayBuffer(canvas: HTMLCanvasElement): ArrayBuffer; +} +``` + +**Example Usage**: + +```typescript +import { WebFShare, ShareHelpers } from '@openwebf/webf-share'; + +// Example 1: Share text with URL +async function shareArticle() { + try { + const success = await WebFShare.shareText({ + title: 'Amazing Article', + text: 'Check out this amazing article about WebF!', + url: 'https://openwebf.com/articles/amazing' + }); + + if (success) { + console.log('Shared successfully!'); + } + } catch (error) { + console.error('Share failed:', error); + } +} + +// Example 2: Share a canvas as image +async function shareCanvas() { + const canvas = document.getElementById('myCanvas') as HTMLCanvasElement; + const imageData = ShareHelpers.canvasToArrayBuffer(canvas); + + try { + await WebFShare.shareImage(imageData); + } catch (error) { + console.error('Failed to share image:', error); + } +} + +// Example 3: Save screenshot +async function saveScreenshot() { + const canvas = document.getElementById('screenshot') as HTMLCanvasElement; + const imageData = ShareHelpers.canvasToArrayBuffer(canvas); + + try { + const saved = await WebFShare.saveScreenshot(imageData); + if (saved) { + alert('Screenshot saved to device!'); + } + } catch (error) { + console.error('Failed to save screenshot:', error); + } +} + +// Example 4: Feature detection +function setupShareButton() { + const shareBtn = document.getElementById('shareBtn'); + + if (typeof WebFShare !== 'undefined') { + shareBtn.onclick = () => shareArticle(); + } else { + // Fallback for environments without the plugin + shareBtn.onclick = () => { + navigator.clipboard.writeText('https://example.com'); + alert('Link copied to clipboard!'); + }; + } +} +``` + +**Platform-Specific Behavior**: + +| Platform | Share Behavior | Screenshot Save Location | +|----------|----------------|-------------------------| +| **Android** | Opens system share sheet | `/storage/emulated/0/Download/` | +| **iOS** | Opens system share sheet | App documents directory (accessible via Files app) | +| **macOS** | Opens system share dialog | Application documents directory | + +**Common Use Cases**: +- Share blog posts or articles to social media +- Share app content with friends +- Save generated images or charts +- Export user-created content +- Share deep links to specific app content + +--- + +## Coming Soon + +The following plugins are under development or planned: + +### Camera & Photos Plugin +Access device camera and photo library for capturing and selecting images. + +### Payment Plugin +Process payments using platform-native payment systems (Apple Pay, Google Pay). + +### Geolocation Plugin +Enhanced geolocation with background tracking and geofencing. + +### Push Notifications Plugin +Send and receive push notifications with rich media support. + +### Biometric Authentication Plugin +Use Face ID, Touch ID, or fingerprint authentication. + +### File Picker Plugin +Select files from device storage with platform-native file pickers. + +### Deep Link Plugin +Handle deep links and universal links for app navigation. + +--- + +## Plugin Development + +Want to create your own plugin? Follow these resources: + +1. **Plugin Development Guide**: https://openwebf.com/en/docs/developer-guide/native-plugins +2. **Example Plugins**: https://github.com/openwebf/webf/tree/main/webf_modules +3. **WebF Module API**: Study existing plugins to understand the module system + +### Plugin Architecture + +A WebF native plugin consists of: + +1. **Flutter Package** (native side): + - Implements native platform functionality + - Extends WebF's module system + - Handles platform-specific code + +2. **npm Package** (JavaScript side): + - Exposes JavaScript/TypeScript APIs + - Provides type definitions + - Documents usage + +3. **Bridge Layer**: + - WebF's module system connects Flutter and JavaScript + - Handles data serialization between platforms + - Manages async communication + +### Best Practices for Plugin Development + +- Follow WebF module conventions +- Provide TypeScript definitions +- Support multiple platforms when possible +- Handle errors gracefully +- Document all APIs clearly +- Include usage examples +- Test on real devices +- Keep APIs simple and consistent + +--- + +## Plugin Compatibility Matrix + +| Plugin | iOS | Android | macOS | Windows | Linux | Web | +|--------|-----|---------|-------|---------|-------|-----| +| Share | ✅ | ✅ | ✅ | ⏳ | ⏳ | ❌ | + +Legend: +- ✅ Fully supported +- ⏳ Coming soon +- ❌ Not applicable + +--- + +## Getting Help + +- **Plugin Issues**: Report on GitHub at https://github.com/openwebf/webf/issues +- **Plugin Requests**: Open a feature request on GitHub +- **Documentation**: Visit https://openwebf.com/en/docs +- **Community**: Join discussions on GitHub Discussions + +--- + +## Contributing Plugins + +Want to contribute a plugin to the WebF ecosystem? + +1. Review the Plugin Development Guide +2. Follow WebF coding standards +3. Include comprehensive tests +4. Document all APIs +5. Submit a pull request to the WebF repository + +We welcome community contributions! + +--- + +**Last Updated**: 2026-01-04 + +**Plugin Registry**: https://openwebf.com/en/native-plugins diff --git a/.agents/skills/webf-native-ui-dev/SKILL.md b/.agents/skills/webf-native-ui-dev/SKILL.md new file mode 100644 index 0000000000..6c721a0a7d --- /dev/null +++ b/.agents/skills/webf-native-ui-dev/SKILL.md @@ -0,0 +1,736 @@ +--- +name: webf-native-ui-dev +description: Develop custom native UI libraries based on Flutter widgets for WebF. Create reusable component libraries that wrap Flutter widgets as web-accessible custom elements. Use when building UI libraries, wrapping Flutter packages, or creating native component systems. +--- + +# WebF Native UI Development + +Want to create your own native UI library for WebF by wrapping Flutter widgets? This skill guides you through the complete process of building custom native UI libraries that make Flutter widgets accessible from JavaScript/TypeScript with React and Vue support. + +## What is Native UI Development? + +Native UI development in WebF means: +- **Wrapping Flutter widgets** as WebF custom elements +- **Bridging native Flutter UI** to web technologies (HTML/JavaScript) +- **Creating reusable component libraries** that work with React, Vue, and vanilla JavaScript +- **Publishing npm packages** with type-safe TypeScript definitions + +## When to Create a Native UI Library + +### Use Cases: +- ✅ You want to expose Flutter widgets to web developers +- ✅ You need to wrap a Flutter package for WebF use +- ✅ You're building a design system with native performance +- ✅ You want to create platform-specific components (iOS, Android, etc.) +- ✅ You need custom widgets beyond HTML/CSS capabilities + +### Don't Create a Native UI Library When: +- ❌ HTML/CSS can achieve the same result (use standard web) +- ❌ You just need to use existing UI libraries (see `webf-native-ui` skill) +- ❌ You're building a one-off component (use WebF widget element directly) + +## Architecture Overview + +A native UI library consists of three layers: + +``` +┌─────────────────────────────────────────┐ +│ JavaScript/TypeScript (React/Vue) │ ← Generated by CLI +│ @openwebf/my-component-lib │ +├─────────────────────────────────────────┤ +│ TypeScript Definitions (.d.ts) │ ← You write this +│ Component interfaces and events │ +├─────────────────────────────────────────┤ +│ Dart (Flutter) │ ← You write this +│ Flutter widget wrappers │ +│ my_component_lib package │ +└─────────────────────────────────────────┘ +``` + +## Development Workflow + +### Overview + +```bash +# 1. Create Flutter package with Dart wrappers +# 2. Write TypeScript definition files +# 3. Generate React/Vue components with WebF CLI +# 4. Test and publish + +webf codegen my-ui-lib --flutter-package-src=./flutter_package +``` + +## Step-by-Step Guide + +### Step 1: Create Flutter Package Structure + +Create a standard Flutter package: + +```bash +# Create Flutter package +flutter create --template=package my_component_lib + +cd my_component_lib +``` + +**Directory structure:** +``` +my_component_lib/ +├── lib/ +│ ├── my_component_lib.dart # Main export file +│ └── src/ +│ ├── button.dart # Dart widget wrapper +│ ├── button.d.ts # TypeScript definitions +│ ├── input.dart +│ └── input.d.ts +├── pubspec.yaml +└── README.md +``` + +**pubspec.yaml dependencies:** +```yaml +dependencies: + flutter: + sdk: flutter + webf: ^0.24.0 # Latest WebF version +``` + +### Step 2: Write Dart Widget Wrappers + +Create a Dart class that wraps your Flutter widget: + +**Example: lib/src/button.dart** + +```dart +import 'package:flutter/widgets.dart'; +import 'package:webf/webf.dart'; +import 'button_bindings_generated.dart'; // Will be generated by CLI + +/// Custom button component wrapping Flutter widgets +class MyCustomButton extends MyCustomButtonBindings { + MyCustomButton(super.context); + + // Internal state + String _variant = 'filled'; + bool _disabled = false; + + // Property getters/setters (implement interface from bindings) + @override + String get variant => _variant; + + @override + set variant(String value) { + _variant = value; + // Trigger rebuild when property changes + setState(() {}); + } + + @override + bool get disabled => _disabled; + + @override + set disabled(bool value) { + _disabled = value; + setState(() {}); + } + + @override + WebFWidgetElementState createState() { + return MyCustomButtonState(this); + } +} + +class MyCustomButtonState extends WebFWidgetElementState { + MyCustomButtonState(super.widgetElement); + + @override + MyCustomButton get widgetElement => super.widgetElement as MyCustomButton; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: widgetElement.disabled ? null : () { + // Dispatch click event to JavaScript + widgetElement.dispatchEvent(Event('click')); + }, + child: Container( + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: _getBackgroundColor(), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + // Get text from child nodes + widgetElement.getTextContent() ?? 'Button', + style: TextStyle( + color: widgetElement.disabled ? Colors.grey : Colors.white, + ), + ), + ), + ); + } + + Color _getBackgroundColor() { + if (widgetElement.disabled) return Colors.grey[400]!; + switch (widgetElement.variant) { + case 'filled': + return Colors.blue; + case 'outlined': + return Colors.transparent; + default: + return Colors.blue; + } + } +} +``` + +### Step 3: Write TypeScript Definitions + +Create a `.d.ts` file alongside your Dart file: + +**Example: lib/src/button.d.ts** + +```typescript +/** + * Custom button component with multiple variants. + */ + +/** + * Properties for . + */ +interface MyCustomButtonProperties { + /** + * Button variant style. + * Supported values: 'filled' | 'outlined' | 'text' + * @default 'filled' + */ + variant?: string; + + /** + * Whether the button is disabled. + * @default false + */ + disabled?: boolean; +} + +/** + * Events for . + */ +interface MyCustomButtonEvents { + /** + * Fired when the button is clicked. + */ + click: Event; +} +``` + +**TypeScript Guidelines:** +- Interface names must end with `Properties` or `Events` +- Use `?` for optional properties (except booleans, which are always non-nullable in Dart) +- Use `CustomEvent` for events with data +- Add JSDoc comments for documentation +- See the [TypeScript Definition Guide](./typescript-guide.md) for more details + +### Step 4: Create Main Export File + +**lib/my_component_lib.dart:** + +```dart +library my_component_lib; + +import 'package:webf/webf.dart'; +import 'src/button.dart'; + +export 'src/button.dart'; + +/// Install all components in this library +void installMyComponentLib() { + // Register custom elements + WebFController.defineCustomElement( + 'my-custom-button', + (context) => MyCustomButton(context), + ); + + // Add more components here + // WebFController.defineCustomElement('my-custom-input', ...); +} +``` + +### Step 5: Generate React/Vue Components + +Use the WebF CLI to generate JavaScript/TypeScript components: + +```bash +# Install WebF CLI globally (if not already installed) +npm install -g @openwebf/webf-cli + +# Generate TypeScript bindings and React/Vue components +webf codegen my-ui-lib-react \ + --flutter-package-src=./my_component_lib \ + --framework=react + +webf codegen my-ui-lib-vue \ + --flutter-package-src=./my_component_lib \ + --framework=vue +``` + +**What the CLI does:** +1. ✅ Parses your `.d.ts` files +2. ✅ Generates Dart binding classes (`*_bindings_generated.dart`) +3. ✅ Creates React components with proper TypeScript types +4. ✅ Creates Vue components with TypeScript support +5. ✅ Copies `.d.ts` files to output directory +6. ✅ Creates `package.json` with correct metadata +7. ✅ Runs `npm run build` if a build script exists + +**Generated output structure:** +``` +my-ui-lib-react/ +├── src/ +│ ├── MyCustomButton.tsx # React component +│ └── index.ts # Main export +├── dist/ # Built files (after npm run build) +├── package.json +├── tsconfig.json +└── README.md +``` + +### Step 6: Test Your Components + +#### Test in Flutter App + +**In your Flutter app's main.dart:** + +```dart +import 'package:my_component_lib/my_component_lib.dart'; + +void main() { + WebFControllerManager.instance.initialize(WebFControllerManagerConfig( + maxAliveInstances: 2, + maxAttachedInstances: 1, + )); + + // Install your component library + installMyComponentLib(); + + runApp(MyApp()); +} +``` + +#### Test in JavaScript/TypeScript + +**React example:** + +```tsx +import { MyCustomButton } from '@openwebf/my-ui-lib-react'; + +function App() { + return ( +
+ console.log('Clicked!')} + > + Click Me + +
+ ); +} +``` + +**Vue example:** + +```vue + + + +``` + +### Step 7: Publish Your Library + +#### Publish Flutter Package + +```bash +# In Flutter package directory +flutter pub publish + +# Or for private packages +flutter pub publish --server=https://your-private-registry.com +``` + +#### Publish npm Packages + +```bash +# Automatic publishing with CLI +webf codegen my-ui-lib-react \ + --flutter-package-src=./my_component_lib \ + --framework=react \ + --publish-to-npm + +# Or manual publishing +cd my-ui-lib-react +npm publish +``` + +**For custom npm registry:** + +```bash +webf codegen my-ui-lib-react \ + --flutter-package-src=./my_component_lib \ + --framework=react \ + --publish-to-npm \ + --npm-registry=https://registry.your-company.com/ +``` + +## Advanced Patterns + +### 1. Handling Complex Properties + +**TypeScript:** +```typescript +interface MyComplexWidgetProperties { + // JSON string properties for complex data + items?: string; // Will be JSON.parse() in Dart + + // Enum-like values + alignment?: 'left' | 'center' | 'right'; + + // Numeric properties + maxLength?: number; + opacity?: number; +} +``` + +**Dart:** +```dart +@override +set items(String? value) { + if (value != null) { + try { + final List parsed = jsonDecode(value); + _items = parsed.cast>(); + setState(() {}); + } catch (e) { + print('Error parsing items: $e'); + } + } +} +``` + +### 2. Dispatching Custom Events with Data + +**Dart:** +```dart +void _handleValueChange(String newValue) { + // Dispatch CustomEvent with data + widgetElement.dispatchEvent(CustomEvent( + 'change', + detail: {'value': newValue}, + )); +} +``` + +**TypeScript:** +```typescript +interface MyInputEvents { + change: CustomEvent<{value: string}>; +} +``` + +### 3. Calling Methods from JavaScript + +**TypeScript:** +```typescript +interface MyInputProperties { + // Regular properties + value?: string; + + // Methods + focus(): void; + clear(): void; +} +``` + +**Dart:** +```dart +class MyInput extends MyInputBindings { + final FocusNode _focusNode = FocusNode(); + + @override + void focus() { + _focusNode.requestFocus(); + } + + @override + void clear() { + // Clear the input + value = ''; + // Dispatch event + dispatchEvent(Event('input')); + } +} +``` + +### 4. Reading CSS Styles + +**Dart:** +```dart +@override +Widget build(BuildContext context) { + // Read CSS properties + final renderStyle = widgetElement.renderStyle; + final backgroundColor = renderStyle.backgroundColor?.value; + final borderRadius = renderStyle.borderRadius; + + return Container( + decoration: BoxDecoration( + color: backgroundColor ?? Colors.blue, + borderRadius: BorderRadius.circular( + borderRadius?.topLeft?.x ?? 8.0 + ), + ), + child: buildChild(), + ); +} +``` + +### 5. Handling Child Elements + +**Dart:** +```dart +@override +Widget build(BuildContext context) { + // Get text content from child nodes + final text = widgetElement.getTextContent() ?? ''; + + // Build child widgets + final children = widgetElement.children.map((child) { + return child.renderObject?.widget ?? SizedBox(); + }).toList(); + + return Column( + children: children, + ); +} +``` + +## Common Patterns and Best Practices + +### 1. Property Validation + +```dart +@override +set variant(String value) { + const validVariants = ['filled', 'outlined', 'text']; + if (validVariants.contains(value)) { + _variant = value; + } else { + print('Warning: Invalid variant "$value"'); + _variant = 'filled'; + } + setState(() {}); +} +``` + +### 2. Debouncing Frequent Updates + +```dart +Timer? _debounceTimer; + +@override +set searchQuery(String value) { + _searchQuery = value; + + // Debounce search + _debounceTimer?.cancel(); + _debounceTimer = Timer(Duration(milliseconds: 300), () { + _performSearch(); + }); +} +``` + +### 3. Lifecycle Management + +```dart +@override +void didMount() { + super.didMount(); + // Called when element is inserted into DOM + _initializeWidget(); +} + +@override +void dispose() { + // Clean up resources + _debounceTimer?.cancel(); + _focusNode.dispose(); + super.dispose(); +} +``` + +### 4. Error Handling + +```dart +@override +set jsonData(String? value) { + if (value == null || value.isEmpty) { + _data = null; + return; + } + + try { + _data = jsonDecode(value); + setState(() {}); + } catch (e) { + print('Error parsing JSON: $e'); + // Dispatch error event + dispatchEvent(CustomEvent('error', detail: {'message': e.toString()})); + } +} +``` + +## CLI Command Reference + +### Basic Generation + +```bash +# Generate React components +webf codegen output-dir --flutter-package-src=./my_package --framework=react + +# Generate Vue components +webf codegen output-dir --flutter-package-src=./my_package --framework=vue + +# Specify package name +webf codegen output-dir \ + --flutter-package-src=./my_package \ + --framework=react \ + --package-name=@mycompany/my-ui-lib +``` + +### Auto-Publishing + +```bash +# Publish to npm after generation +webf codegen output-dir \ + --flutter-package-src=./my_package \ + --framework=react \ + --publish-to-npm + +# Publish to custom registry +webf codegen output-dir \ + --flutter-package-src=./my_package \ + --framework=react \ + --publish-to-npm \ + --npm-registry=https://registry.company.com/ +``` + +### Project Auto-Creation + +The CLI auto-creates projects if files are missing: +- If `package.json`, `tsconfig.json`, or `global.d.ts` are missing, it creates a new project +- If framework is not specified, it prompts interactively +- Uses existing configuration if project already exists + +## Troubleshooting + +### Issue: Bindings file not found + +**Error:** `Error: Could not find 'button_bindings_generated.dart'` + +**Solution:** +1. Make sure you've run the CLI code generation +2. Check that `.d.ts` files are in the same directory as `.dart` files +3. Verify interface naming (must end with `Properties` or `Events`) + +### Issue: Properties not updating in UI + +**Cause:** Not calling `setState()` after property changes + +**Solution:** +```dart +@override +set myProperty(String value) { + _myProperty = value; + setState(() {}); // ← Don't forget this! +} +``` + +### Issue: Events not firing in JavaScript + +**Cause:** Event name mismatch or not dispatching events + +**Solution:** +```dart +// Make sure event names match your TypeScript definitions +widgetElement.dispatchEvent(Event('click')); // matches 'click' in TypeScript +``` + +### Issue: TypeScript types not working + +**Cause:** Generated types not exported properly + +**Solution:** Check that generated `package.json` exports types: +```json +{ + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + } +} +``` + +## Complete Example: Text Input Component + +See [Complete Example](./example-input.md) for a full implementation of a text input component with: +- Flutter TextFormField wrapper +- TypeScript definitions +- Event handling +- Validation +- Methods (focus, blur, clear) +- CSS integration + +## Resources + +- **CLI Development Guide**: [cli/AGENTS.md](https://github.com/openwebf/webf/blob/main/cli/AGENTS.md) +- **TypeScript Guide**: [CLI TYPING_GUIDE.md](https://github.com/openwebf/webf/blob/main/cli/TYPING_GUIDE.md) +- **Example Package**: [native_uis/webf_cupertino_ui](https://github.com/openwebf/webf/tree/main/native_uis/webf_cupertino_ui) +- **WebF Architecture**: [docs/ARCHITECTURE.md](https://github.com/openwebf/webf/blob/main/docs/ARCHITECTURE.md) +- **Official Documentation**: https://openwebf.com/en/docs/developer-guide/native-ui + +## Next Steps + +After creating your native UI library: + +1. **Test thoroughly** on all target platforms (iOS, Android, desktop) +2. **Write documentation** for each component (see existing `.md` files in webf_cupertino_ui) +3. **Create example apps** demonstrating usage +4. **Publish to pub.dev** (Flutter) and npm (JavaScript) +5. **Maintain compatibility** with WebF version updates + +## Summary + +- ✅ Native UI libraries wrap Flutter widgets as web-accessible custom elements +- ✅ Write Dart wrappers extending WebFWidgetElement +- ✅ Write TypeScript definitions (.d.ts) for each component +- ✅ Use WebF CLI to generate React/Vue components +- ✅ Test in both Flutter and JavaScript environments +- ✅ Publish to pub.dev (Flutter) and npm (JavaScript) +- ✅ Follow existing patterns from webf_cupertino_ui for reference diff --git a/.agents/skills/webf-native-ui-dev/example-input.md b/.agents/skills/webf-native-ui-dev/example-input.md new file mode 100644 index 0000000000..a8baf06e9d --- /dev/null +++ b/.agents/skills/webf-native-ui-dev/example-input.md @@ -0,0 +1,589 @@ +# Complete Example: Text Input Component + +This is a complete, working example of a hybrid UI component that wraps Flutter's TextFormField. + +## File Structure + +``` +my_component_lib/ +└── lib/ + └── src/ + ├── text_input.dart # Dart implementation + ├── text_input.d.ts # TypeScript definitions + └── text_input_bindings_generated.dart # Generated by CLI +``` + +## Step 1: TypeScript Definitions + +**File: `lib/src/text_input.d.ts`** + +```typescript +/** + * A text input component with validation and multiple keyboard types. + */ + +type int = number; + +/** + * Properties for . + */ +interface MyTextInputProperties { + /** + * Current text value. + */ + value?: string; + + /** + * Placeholder text shown when empty. + */ + placeholder?: string; + + /** + * Maximum character length. + */ + maxLength?: int; + + /** + * Input type for keyboard. + * Supported: 'text', 'email', 'number', 'phone', 'url' + * @default 'text' + */ + type?: string; + + /** + * Whether input is disabled. + * @default false + */ + disabled?: boolean; + + /** + * Whether input is read-only. + * @default false + */ + readonly?: boolean; + + /** + * Auto-focus on mount. + * @default false + */ + autofocus?: boolean; + + /** + * Obscure text (for passwords). + * @default false + */ + obscureText?: boolean; + + /** + * Error message to display. + */ + errorText?: string; + + /** + * Helper text to display below input. + */ + helperText?: string; + + // Methods + + /** + * Programmatically focus the input. + */ + focus(): void; + + /** + * Programmatically blur the input. + */ + blur(): void; + + /** + * Clear the current value. + */ + clear(): void; + + /** + * Get the current value. + */ + getValue(): string; + + /** + * Set a new value. + */ + setValue(value: string): void; +} + +/** + * Events for . + */ +interface MyTextInputEvents { + /** + * Fired on every text change. + * detail.value = current text + */ + input: CustomEvent; + + /** + * Fired when Enter is pressed. + * detail.value = current text + */ + submit: CustomEvent; + + /** + * Fired when input gains focus. + */ + focus: Event; + + /** + * Fired when input loses focus. + */ + blur: Event; + + /** + * Fired when validation fails. + * detail.message = error message + */ + error: CustomEvent; +} +``` + +## Step 2: Dart Implementation + +**File: `lib/src/text_input.dart`** + +```dart +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:webf/webf.dart'; +import 'text_input_bindings_generated.dart'; + +class MyTextInput extends MyTextInputBindings { + MyTextInput(super.context); + + // Internal state + String _value = ''; + String _placeholder = ''; + int? _maxLength; + String _type = 'text'; + bool _disabled = false; + bool _readonly = false; + bool _autofocus = false; + bool _obscureText = false; + String? _errorText; + String? _helperText; + + // Property getters/setters + @override + String get value => _value; + + @override + set value(String val) { + _value = val; + setState(() {}); + } + + @override + String get placeholder => _placeholder; + + @override + set placeholder(String val) { + _placeholder = val; + setState(() {}); + } + + @override + String? get maxLength => _maxLength?.toString(); + + @override + set maxLength(String? val) { + _maxLength = val != null ? int.tryParse(val) : null; + setState(() {}); + } + + @override + String get type => _type; + + @override + set type(String val) { + _type = val; + setState(() {}); + } + + @override + bool get disabled => _disabled; + + @override + set disabled(bool val) { + _disabled = val; + setState(() {}); + } + + @override + bool get readonly => _readonly; + + @override + set readonly(bool val) { + _readonly = val; + setState(() {}); + } + + @override + bool get autofocus => _autofocus; + + @override + set autofocus(bool val) { + _autofocus = val; + setState(() {}); + } + + @override + bool get obscureText => _obscureText; + + @override + set obscureText(bool val) { + _obscureText = val; + setState(() {}); + } + + @override + String? get errorText => _errorText; + + @override + set errorText(String? val) { + _errorText = val; + setState(() {}); + } + + @override + String? get helperText => _helperText; + + @override + set helperText(String? val) { + _helperText = val; + setState(() {}); + } + + // Methods + @override + void focus() { + final state = this.state as MyTextInputState?; + state?._focusNode.requestFocus(); + } + + @override + void blur() { + final state = this.state as MyTextInputState?; + state?._focusNode.unfocus(); + } + + @override + void clear() { + value = ''; + dispatchEvent(CustomEvent('input', detail: {'value': ''})); + } + + @override + String getValue() { + return _value; + } + + @override + void setValue(String val) { + value = val; + } + + @override + WebFWidgetElementState createState() { + return MyTextInputState(this); + } +} + +class MyTextInputState extends WebFWidgetElementState { + MyTextInputState(super.widgetElement); + + final FocusNode _focusNode = FocusNode(); + late TextEditingController _controller; + + @override + MyTextInput get widgetElement => super.widgetElement as MyTextInput; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(text: widgetElement.value); + + // Listen for focus changes + _focusNode.addListener(_handleFocusChange); + } + + @override + void didUpdateWidget() { + super.didUpdateWidget(); + // Update controller if value changed externally + if (_controller.text != widgetElement.value) { + _controller.text = widgetElement.value; + } + } + + @override + void dispose() { + _focusNode.removeListener(_handleFocusChange); + _focusNode.dispose(); + _controller.dispose(); + super.dispose(); + } + + void _handleFocusChange() { + if (_focusNode.hasFocus) { + widgetElement.dispatchEvent(Event('focus')); + } else { + widgetElement.dispatchEvent(Event('blur')); + } + } + + TextInputType _getKeyboardType() { + switch (widgetElement.type) { + case 'email': + return TextInputType.emailAddress; + case 'number': + return TextInputType.number; + case 'phone': + return TextInputType.phone; + case 'url': + return TextInputType.url; + default: + return TextInputType.text; + } + } + + @override + Widget build(BuildContext context) { + return TextFormField( + controller: _controller, + focusNode: _focusNode, + enabled: !widgetElement.disabled, + readOnly: widgetElement.readonly, + autofocus: widgetElement.autofocus, + obscureText: widgetElement.obscureText, + keyboardType: _getKeyboardType(), + maxLength: widgetElement._maxLength, + decoration: InputDecoration( + hintText: widgetElement.placeholder, + errorText: widgetElement.errorText, + helperText: widgetElement.helperText, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), + ), + onChanged: (value) { + widgetElement._value = value; + widgetElement.dispatchEvent(CustomEvent( + 'input', + detail: {'value': value}, + )); + }, + onFieldSubmitted: (value) { + widgetElement.dispatchEvent(CustomEvent( + 'submit', + detail: {'value': value}, + )); + }, + validator: (value) { + // Custom validation can be added here + if (widgetElement._maxLength != null && + value != null && + value.length > widgetElement._maxLength!) { + final error = 'Maximum ${widgetElement._maxLength} characters'; + widgetElement.dispatchEvent(CustomEvent( + 'error', + detail: {'message': error}, + )); + return error; + } + return null; + }, + ); + } +} +``` + +## Step 3: Generate React/Vue Components + +```bash +# Generate React components +webf codegen my-ui-lib-react \ + --flutter-package-src=./my_component_lib \ + --framework=react \ + --package-name=@mycompany/my-ui-lib + +# Generate Vue components +webf codegen my-ui-lib-vue \ + --flutter-package-src=./my_component_lib \ + --framework=vue \ + --package-name=@mycompany/my-ui-lib-vue +``` + +## Step 4: Usage Examples + +### React Usage + +```tsx +import { MyTextInput } from '@mycompany/my-ui-lib'; +import { useState } from 'react'; + +function LoginForm() { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + + const handleSubmit = () => { + if (!username || !password) { + setError('Please fill in all fields'); + return; + } + // Perform login + console.log('Login:', { username, password }); + }; + + return ( +
+ setUsername(e.detail.value)} + errorText={error} + /> + + setPassword(e.detail.value)} + onSubmit={handleSubmit} + /> + + +
+ ); +} +``` + +### Vue Usage + +```vue + + + +``` + +### Vanilla JavaScript Usage + +```html + + + +``` + +## Step 5: Register in Flutter App + +**main.dart:** + +```dart +import 'package:flutter/material.dart'; +import 'package:webf/webf.dart'; +import 'package:my_component_lib/my_component_lib.dart'; + +void main() { + WebFControllerManager.instance.initialize(WebFControllerManagerConfig( + maxAliveInstances: 2, + maxAttachedInstances: 1, + )); + + // Register custom element + WebFController.defineCustomElement( + 'my-text-input', + (context) => MyTextInput(context), + ); + + runApp(MyApp()); +} +``` + +## Key Takeaways + +1. ✅ **TypeScript definitions** define the component interface +2. ✅ **Dart implementation** wraps Flutter widget (TextFormField) +3. ✅ **State management** uses `setState()` to trigger rebuilds +4. ✅ **Event dispatching** communicates changes back to JavaScript +5. ✅ **Methods** provide programmatic control from JavaScript +6. ✅ **CLI generation** creates type-safe React/Vue components +7. ✅ **Registration** makes component available in Flutter app + +This pattern can be applied to wrap any Flutter widget as a WebF custom element! diff --git a/.agents/skills/webf-native-ui-dev/typescript-guide.md b/.agents/skills/webf-native-ui-dev/typescript-guide.md new file mode 100644 index 0000000000..8a3cffb441 --- /dev/null +++ b/.agents/skills/webf-native-ui-dev/typescript-guide.md @@ -0,0 +1,111 @@ +# TypeScript Definition Guide for Hybrid UI Components + +Complete reference for writing `.d.ts` files that will be parsed by the WebF CLI to generate Dart bindings and React/Vue components. + +## Interface Naming Convention + +**Required pattern:** +```typescript +interface Properties { ... } +interface Events { ... } +``` + +The component name is automatically derived by removing the "Properties" or "Events" suffix. + +## Type Mappings + +| TypeScript Type | Dart Type | Notes | +|----------------|-----------|-------| +| `string` | `String` | Nullable if optional | +| `number` | `double` | For floating point | +| `int` | `int` | Use `type int = number` alias | +| `boolean` | `bool` | **Always non-nullable** | +| `any` | `dynamic` | Avoid when possible | +| `void` | `void` | For methods only | + +## Properties Interface + +```typescript +interface MyComponentProperties { + // Required string property + id: string; + + // Optional string property (nullable in Dart) + title?: string; + + // Boolean property (non-nullable in Dart even with ?) + disabled?: boolean; + + // Numeric properties + width?: number; // → double in Dart + maxCount?: int; // → int in Dart + + // Kebab-case properties (quoted) + 'pressed-opacity'?: string; + 'active-color'?: string; + + // Complex data as JSON strings + items?: string; // Will be parsed with jsonDecode() + + // Methods + focus(): void; + getValue(): string; +} +``` + +## Events Interface + +```typescript +interface MyComponentEvents { + // Standard DOM event (no data) + click: Event; + + // CustomEvent with typed data + change: CustomEvent; + select: CustomEvent; + toggle: CustomEvent; +} +``` + +## Complete Example + +```typescript +/** + * A text input component with validation. + */ +type int = number; + +interface MyInputProperties { + value?: string; + placeholder?: string; + maxLength?: int; + disabled?: boolean; + + focus(): void; + blur(): void; + clear(): void; +} + +interface MyInputEvents { + input: CustomEvent; + submit: CustomEvent; + focus: Event; + blur: Event; +} +``` + +## Best Practices + +- ✅ Use JSDoc comments for documentation +- ✅ Document default values with `@default` +- ✅ Keep event names simple and descriptive +- ✅ Use kebab-case for HTML-style attributes +- ✅ Group related properties with comments +- ❌ Don't use arrays or objects (use JSON strings) +- ❌ Don't use arrow functions for methods + +## Resources + +- **WebF CLI Guide**: [../../cli/CLAUDE.md](../../cli/CLAUDE.md) +- **CLI Typing Guide**: [../../cli/TYPING_GUIDE.md](../../cli/TYPING_GUIDE.md) +- **Example Components**: [../../native_uis/webf_cupertino_ui/lib/src/](../../native_uis/webf_cupertino_ui/lib/src/) diff --git a/.agents/skills/webf-native-ui/SKILL.md b/.agents/skills/webf-native-ui/SKILL.md new file mode 100644 index 0000000000..e7ce3f156c --- /dev/null +++ b/.agents/skills/webf-native-ui/SKILL.md @@ -0,0 +1,348 @@ +--- +name: webf-native-ui +description: Setup and use WebF's Cupertino UI library to build native iOS-style UIs with pre-built components instead of crafting everything with HTML/CSS. Use when building iOS apps, adding native UI components, or improving UI performance. +--- + +# WebF Native UI Libraries + +Instead of crafting all UIs with HTML/CSS, WebF provides **pre-built native UI libraries** that render as native Flutter widgets with full native performance. These components look and feel native on each platform while being controlled from your JavaScript code. + +## What Are Native UI Libraries? + +Native UI libraries are collections of UI components that: +- **Render as native Flutter widgets** (not DOM elements) +- **Look and feel native** on each platform (iOS, Android, etc.) +- **Provide better performance** than HTML/CSS for complex UIs +- **Use platform-specific design** (Cupertino for iOS, Material for Android) +- **Work with React, Vue, and vanilla JavaScript** + +## Available Library + +### Cupertino UI ✅ + +**Description**: iOS-style components following Apple's Human Interface Guidelines + +**Platforms**: iOS, macOS (optimized for iOS design) + +**Component Count**: 30+ components + +**Available Components**: +- **Navigation & Layout**: Tab, Scaffold, TabBar, TabView +- **Dialogs & Sheets**: Alert Dialog, Action Sheet, Modal Popup, Context Menu +- **Lists**: List Section, List Tile +- **Forms**: Form Section, Form Row, TextField, Search Field +- **Pickers**: Date Picker, Time Picker +- **Controls**: Button, Switch, Slider, Segmented Control, Checkbox, Radio +- **Icons**: 1000+ SF Symbols +- **Colors**: Cupertino color system + +**NPM Packages**: +- React: `@openwebf/react-cupertino-ui` +- Vue: `@openwebf/vue-cupertino-ui` + +**Flutter Package**: `webf_cupertino_ui` + +**Documentation**: https://openwebf.com/en/ui-components/cupertino + +--- + +## When to Use Native UI vs HTML/CSS + +### Use Cupertino UI When: +- ✅ Building iOS-style apps +- ✅ Need native-looking iOS forms, buttons, and controls +- ✅ Want 60fps native performance for complex UIs +- ✅ Building iOS lists, dialogs, or navigation patterns +- ✅ Need Apple's Human Interface Guidelines design language + +### Use HTML/CSS When: +- ✅ Building custom designs that don't follow platform patterns +- ✅ Using existing web component libraries (e.g., Tailwind CSS) +- ✅ Need maximum flexibility in styling +- ✅ Porting existing web apps +- ✅ Building cross-platform designs (not platform-specific) + +## Setup Instructions + +### Step 1: Configure Flutter Project (Optional) + +If you have access to the Flutter project hosting your WebF app: + +**For Cupertino UI:** +1. Open your Flutter project's `pubspec.yaml` +2. Add the dependency: + ```yaml + dependencies: + webf_cupertino_ui: ^1.0.0 + ``` +3. Run: `flutter pub get` +4. Initialize in your main Dart file: + ```dart + import 'package:webf/webf.dart'; + import 'package:webf_cupertino_ui/webf_cupertino_ui.dart'; + + void main() { + WebFControllerManager.instance.initialize(WebFControllerManagerConfig( + maxAliveInstances: 2, + maxAttachedInstances: 1, + )); + + // Install Cupertino UI components + installWebFCupertinoUI(); + + runApp(MyApp()); + } + ``` + +### Step 2: Install NPM Packages (JavaScript/TypeScript) + +**For React:** +```bash +npm install @openwebf/react-cupertino-ui +``` + +**For Vue:** +```bash +npm install @openwebf/vue-cupertino-ui +``` + +### Step 3: Using Components in Your Code + +**React Example:** +```tsx +import { FlutterCupertinoButton, FlutterCupertinoTextField } from '@openwebf/react-cupertino-ui'; + +export function MyComponent() { + return ( +
+ console.log('Value:', value)} + /> + console.log('Clicked')} + > + Submit + +
+ ); +} +``` + +**Vue Example:** +```vue + + + +``` + +## Component Reference + +See the [Native UI Component Reference](./reference.md) for a complete list of available components and their properties. + +## Common Patterns + +### 1. Building an iOS-Style Form + +```tsx +import { + FlutterCupertinoFormSection, + FlutterCupertinoFormRow, + FlutterCupertinoTextField, + FlutterCupertinoButton +} from '@openwebf/react-cupertino-ui'; + +export function ProfileForm() { + return ( + + + + + + + + + Save Changes + + + ); +} +``` + +### 2. Building a Settings Screen + +```tsx +import { + FlutterCupertinoListSection, + FlutterCupertinoListTile, + FlutterCupertinoSwitch +} from '@openwebf/react-cupertino-ui'; + +export function SettingsScreen() { + return ( + + } + /> + } + /> + + ); +} +``` + +### 3. Showing a Native Dialog + +```tsx +import { FlutterCupertinoAlertDialog } from '@openwebf/react-cupertino-ui'; + +export function ConfirmationDialog({ onConfirm, onCancel }) { + return ( + + ); +} +``` + +## Best Practices + +### 1. Mix Native UI with HTML/CSS +You don't have to use native UI everywhere. Mix and match: +```tsx +// Use native components for platform-specific UIs + + Save + + +// Use HTML/CSS for custom layouts +
+

Custom Design

+

This uses regular HTML/CSS

+
+``` + +### 2. Use Native UI for Complex Components +Native UI components handle complex interactions better: +- Date pickers → Use `FlutterCupertinoDatePicker` instead of HTML input +- Sliders → Use `FlutterCupertinoSlider` for native feel +- Segmented controls → Use `FlutterCupertinoSegmentedControl` + +### 3. Check Component Documentation +Always check the official documentation for component props and events: +- https://openwebf.com/en/ui-components/cupertino + +### 4. Use TypeScript for Type Safety +All native UI packages include TypeScript definitions: +```tsx +import type { FlutterCupertinoButtonProps } from '@openwebf/react-cupertino-ui'; + +const buttonProps: FlutterCupertinoButtonProps = { + variant: 'filled', + onClick: () => console.log('Clicked') +}; +``` + +## Troubleshooting + +### Issue: Components Not Rendering + +**Cause**: Flutter package not installed or initialized + +**Solution**: +1. Check that the Flutter package is in `pubspec.yaml` +2. Verify `installWebFCupertinoUI()` is called in main.dart +3. Run `flutter pub get` +4. Rebuild your Flutter app + +### Issue: TypeScript Errors for Components + +**Cause**: NPM package not installed correctly + +**Solution**: +```bash +# Reinstall the package +npm install @openwebf/react-cupertino-ui --save + +# Clear node_modules and reinstall +rm -rf node_modules package-lock.json +npm install +``` + +### Issue: Vue Components Not Found + +**Cause**: Vue bindings need to be generated + +**Solution**: Follow the "For Vue + Cupertino UI" steps in Step 2 above to generate Vue bindings using `webf codegen`. + +### Issue: Props Don't Match Flutter Widget + +**Cause**: WebF automatically converts between JavaScript and Dart naming + +**Solution**: +- JavaScript uses camelCase: `onClick`, `onChange`, `placeholder` +- Flutter uses camelCase too, so props map directly +- Check documentation for exact prop names + +## Resources + +- **Component Gallery**: https://openwebf.com/en/ui-components +- **Cupertino UI Docs**: https://openwebf.com/en/ui-components/cupertino +- **WebF CLI Docs**: https://openwebf.com/en/docs/tools/webf-cli +- **React Examples**: https://github.com/openwebf/react-cupertino-gallery +- **Vue Examples**: https://github.com/openwebf/vue-cupertino-gallery + +## Next Steps + +After setting up native UI: + +1. **Explore components**: Visit https://openwebf.com/en/ui-components to see all available components +2. **Check examples**: Look at the gallery apps for React and Vue +3. **Mix with HTML/CSS**: Use native UI where it makes sense, HTML/CSS elsewhere +4. **Performance**: Native UI components render at 60fps with Flutter-level performance + +## Summary + +- ✅ Native UI libraries provide pre-built, platform-specific components +- ✅ Cupertino UI for iOS-style apps (30+ components available now) +- ✅ Form UI for validated forms (available now) +- ✅ Material UI coming soon for Android-style apps +- ✅ Install Flutter packages first, then npm packages +- ✅ Mix native UI with HTML/CSS as needed +- ✅ Better performance than HTML/CSS for complex UIs +- ✅ Full React and Vue support diff --git a/.agents/skills/webf-native-ui/reference.md b/.agents/skills/webf-native-ui/reference.md new file mode 100644 index 0000000000..4744846e38 --- /dev/null +++ b/.agents/skills/webf-native-ui/reference.md @@ -0,0 +1,562 @@ +# Native UI Component Reference + +This reference lists all available native UI components in WebF's native UI libraries. + +## Cupertino UI Components (iOS-style) + +### Navigation & Layout + +#### FlutterCupertinoTabScaffold +Creates a tabbed application structure with tab bar. + +**Props**: +- `tabs` - Array of tab definitions +- `initialIndex` - Starting tab index (default: 0) + +**Example**: +```tsx + +``` + +#### FlutterCupertinoTabBar +Bottom tab bar for navigation. + +**Props**: +- `items` - Array of tab bar items +- `currentIndex` - Active tab index +- `onTap` - Callback when tab is tapped + +**Example**: +```tsx + console.log('Tab:', index)} +/> +``` + +#### FlutterCupertinoTabView +Content container for tab views. + +**Props**: +- `children` - Tab content +- `builder` - Optional builder function + +--- + +### Buttons & Actions + +#### FlutterCupertinoButton +iOS-style button with multiple variants. + +**Props**: +- `variant` - Button style: `'filled'`, `'text'`, or `'plain'` (default: `'text'`) +- `onClick` - Click handler +- `disabled` - Disable button (default: false) +- `children` - Button label/content +- `color` - Custom button color +- `padding` - Custom padding + +**Example**: +```tsx + console.log('Clicked')} + disabled={false} +> + Continue + +``` + +--- + +### Dialogs & Overlays + +#### FlutterCupertinoAlertDialog +Native iOS alert dialog. + +**Props**: +- `title` - Dialog title +- `content` - Dialog message/content +- `actions` - Array of action buttons + - `label` - Button text + - `onPress` - Button callback + - `isDestructive` - Red destructive style (default: false) + - `isDefaultAction` - Bold default style (default: false) + +**Example**: +```tsx + {} }, + { label: 'Delete', onPress: () => {}, isDestructive: true } + ]} +/> +``` + +#### FlutterCupertinoActionSheet +Bottom sheet with action options. + +**Props**: +- `title` - Sheet title +- `message` - Sheet message +- `actions` - Array of action buttons +- `cancelButton` - Cancel button configuration + +**Example**: +```tsx + {} }, + { label: 'Option 2', onPress: () => {} } + ]} + cancelButton={{ label: 'Cancel', onPress: () => {} }} +/> +``` + +#### FlutterCupertinoModalPopup +Generic modal popup container. + +**Props**: +- `children` - Popup content +- `onDismiss` - Callback when dismissed + +**Example**: +```tsx + console.log('Dismissed')}> +
+

Modal Content

+
+
+``` + +#### FlutterCupertinoContextMenu +Long-press context menu. + +**Props**: +- `actions` - Array of menu actions +- `children` - Trigger element + +**Example**: +```tsx + {} }, + { label: 'Delete', onPress: () => {}, isDestructive: true } + ]} +> + Photo + +``` + +--- + +### Lists + +#### FlutterCupertinoListSection +Grouped list section with header/footer. + +**Props**: +- `header` - Section header text +- `footer` - Section footer text +- `children` - List items (FlutterCupertinoListTile) + +**Example**: +```tsx + + + + +``` + +#### FlutterCupertinoListTile +Individual list item. + +**Props**: +- `title` - Main title text +- `subtitle` - Optional subtitle text +- `leading` - Leading widget/icon +- `trailing` - Trailing widget/icon +- `onTap` - Tap callback +- `additionalInfo` - Right-side info text + +**Example**: +```tsx +} + onTap={() => console.log('Tapped')} +/> +``` + +--- + +### Forms + +#### FlutterCupertinoFormSection +Form section container with header. + +**Props**: +- `header` - Section header text +- `footer` - Section footer text +- `children` - Form rows + +**Example**: +```tsx + + + + + +``` + +#### FlutterCupertinoFormRow +Form row with label and input. + +**Props**: +- `label` - Row label text +- `children` - Input widget +- `error` - Error message text +- `helper` - Helper text + +**Example**: +```tsx + + + +``` + +--- + +### Text Input + +#### FlutterCupertinoTextField +Native iOS text field. + +**Props**: +- `placeholder` - Placeholder text +- `value` - Current value +- `onChanged` - Change handler +- `keyboardType` - Keyboard type: `'text'`, `'email'`, `'number'`, `'phone'`, `'url'` +- `obscureText` - Hide text (for passwords, default: false) +- `maxLines` - Maximum lines (default: 1) +- `readOnly` - Read-only mode (default: false) +- `autofocus` - Auto-focus on mount (default: false) +- `clearButtonMode` - Show clear button: `'never'`, `'always'`, `'editing'`, `'notEditing'` + +**Example**: +```tsx + setName(value)} + keyboardType="text" + clearButtonMode="editing" +/> +``` + +#### FlutterCupertinoSearchTextField +Search field with search icon and clear button. + +**Props**: +- `placeholder` - Placeholder text (default: "Search") +- `value` - Current value +- `onChanged` - Change handler +- `onSubmitted` - Submit handler + +**Example**: +```tsx + setSearchQuery(value)} + onSubmitted={(value) => performSearch(value)} +/> +``` + +--- + +### Pickers + +#### FlutterCupertinoDatePicker +Native iOS date picker. + +**Props**: +- `mode` - Picker mode: `'date'`, `'time'`, `'dateTime'` +- `initialDate` - Starting date (ISO string) +- `minimumDate` - Minimum selectable date +- `maximumDate` - Maximum selectable date +- `onDateTimeChanged` - Date change handler + +**Example**: +```tsx + console.log('Selected:', date)} +/> +``` + +--- + +### Controls + +#### FlutterCupertinoSwitch +Native iOS toggle switch. + +**Props**: +- `value` - Switch state (boolean) +- `onChanged` - Change handler +- `activeColor` - Color when active + +**Example**: +```tsx + setIsEnabled(value)} + activeColor="#007AFF" +/> +``` + +#### FlutterCupertinoSlider +Native iOS slider. + +**Props**: +- `value` - Current value (0.0 to 1.0) +- `onChanged` - Change handler +- `min` - Minimum value (default: 0.0) +- `max` - Maximum value (default: 1.0) +- `divisions` - Number of discrete divisions +- `activeColor` - Track color when active + +**Example**: +```tsx + setVolume(value)} + min={0} + max={100} + divisions={100} +/> +``` + +#### FlutterCupertinoSegmentedControl +Segmented control for mutually exclusive choices. + +**Props**: +- `children` - Map of segment widgets `{ '0': , '1': }` +- `groupValue` - Currently selected value +- `onValueChanged` - Selection change handler + +**Example**: +```tsx +Day, + '1': Week, + '2': Month + }} + groupValue={selectedPeriod} + onValueChanged={(value) => setSelectedPeriod(value)} +/> +``` + +#### FlutterCupertinoCheckbox +iOS-style checkbox. + +**Props**: +- `value` - Checked state (boolean) +- `onChanged` - Change handler +- `activeColor` - Color when checked + +**Example**: +```tsx + setIsChecked(value)} +/> +``` + +#### FlutterCupertinoRadio +iOS-style radio button. + +**Props**: +- `value` - Radio value +- `groupValue` - Selected value in group +- `onChanged` - Change handler + +**Example**: +```tsx + setSelectedOption(value)} +/> +``` + +--- + +### Icons & Colors + +#### FlutterCupertinoIcon +iOS SF Symbols icons. + +**Props**: +- `name` - SF Symbol name (e.g., `'house'`, `'person'`, `'gear'`) +- `size` - Icon size (default: 28) +- `color` - Icon color + +**Example**: +```tsx + +``` + +**Common SF Symbols**: +- Navigation: `'house'`, `'magnifyingglass'`, `'person'`, `'gear'`, `'ellipsis'` +- Actions: `'plus'`, `'minus'`, `'checkmark'`, `'xmark'`, `'trash'` +- Media: `'play.fill'`, `'pause.fill'`, `'heart.fill'`, `'star.fill'` +- Communication: `'message.fill'`, `'envelope.fill'`, `'phone.fill'` + +--- + +## Component Naming Conventions + +### React +All components use PascalCase with `Flutter` prefix: +```tsx +import { FlutterCupertinoButton } from '@openwebf/react-cupertino-ui'; + +Click Me +``` + +### Vue +Components use PascalCase (not kebab-case) in templates: +```vue + + + +``` + +--- + +## TypeScript Support + +All native UI packages include full TypeScript definitions: + +```tsx +import type { + FlutterCupertinoButtonProps, + FlutterCupertinoTextFieldProps, + FlutterCupertinoSwitchProps +} from '@openwebf/react-cupertino-ui'; + +const buttonProps: FlutterCupertinoButtonProps = { + variant: 'filled', + onClick: () => console.log('Clicked') +}; +``` + +--- + +## Event Handlers + +### Common Event Props +- `onClick` / `onTap` - Tap/click events +- `onChanged` - Value change events +- `onSubmitted` - Form submission events +- `onDismiss` - Dismiss/close events +- `onFocus` / `onBlur` - Focus events + +### Event Data +Most events pass the new value directly: +```tsx +// Switch + setEnabled(newValue)} +/> + +// Text field + setText(newText)} +/> +``` + +--- + +## Styling Native Components + +### Using CSS Classes +Native components can have CSS classes for layout: +```tsx + + Click Me + +``` + +```css +.my-button-wrapper { + margin: 20px; + width: 200px; +} +``` + +### Custom Colors +Many components accept color props: +```tsx + + Delete + + + +``` + +### Note on Styling Limitations +Native UI components render as Flutter widgets, so: +- ✅ Can control: position, size, margin, padding (via wrapper) +- ❌ Cannot control: internal styling, fonts, borders (use Flutter theming) + +--- + +## Resources + +- **Official Documentation**: https://openwebf.com/en/ui-components +- **Cupertino Gallery**: https://openwebf.com/en/ui-components/cupertino +- **SF Symbols Browser**: https://developer.apple.com/sf-symbols/ +- **Form Validation**: https://pub.dev/packages/form_builder_validators + +--- + +## Getting Help + +- **Component not working?** Check that the Flutter package is installed and initialized +- **TypeScript errors?** Ensure the npm package is installed correctly +- **Props not working?** Check the official documentation for the exact prop names +- **Vue components?** Generate bindings using `webf codegen` command diff --git a/.agents/skills/webf-quickstart/SKILL.md b/.agents/skills/webf-quickstart/SKILL.md new file mode 100644 index 0000000000..778704c15c --- /dev/null +++ b/.agents/skills/webf-quickstart/SKILL.md @@ -0,0 +1,222 @@ +--- +name: webf-quickstart +description: Get started with WebF development - setup WebF Go, create a React/Vue/Svelte project with Vite, and load your first app. Use when starting a new WebF project, onboarding new developers, or setting up development environment. +--- + +# WebF Quickstart + +> **Note**: Building WebF apps is nearly identical to building regular web apps with Vite + React/Vue/Svelte. The only difference is you replace your browser with **WebF Go** for testing during development. Everything else - project structure, build tools, testing frameworks, and deployment - works the same way. + +> **⚠️ IMPORTANT**: WebF Go is for **development and testing ONLY**. For production, you must build a Flutter app with WebF integration. Do NOT distribute WebF Go to end users. + +Get up and running with WebF in minutes. This skill guides you through setting up your development environment, creating your first project, and loading it in WebF Go. + +## What You Need + +**Only Node.js is required** - that's it! + +- Node.js (LTS version recommended) from [nodejs.org](https://nodejs.org/) +- **You do NOT need**: Flutter SDK, Xcode, or Android Studio + +WebF lets web developers build native apps using familiar web tools. + +## Step-by-Step Setup + +### 1. Download WebF Go (For Testing Only) + +WebF Go is a pre-built native app containing the WebF rendering engine. It's designed for **development and testing purposes only** - not for production deployment. + +**For Desktop Development:** +- Download WebF Go for your OS (macOS, Windows, Linux) +- Get it from: **https://openwebf.com/en/go** + +**For Mobile Testing:** +- iOS: Download from App Store +- Android: Download from Google Play + +**Remember**: WebF Go is a testing tool. For production apps, you'll need to build a Flutter app with WebF integration. + +Launch WebF Go - you'll see an input field ready to load your app. + +### 2. Create Your Project with Vite + +Open your terminal and create a new project: + +```bash +npm create vite@latest my-webf-app +``` + +Vite will prompt you to: +1. Choose a framework: **React**, **Vue**, **Svelte**, etc. +2. Choose a variant (JavaScript or TypeScript) + +### 3. Install Dependencies and Start Dev Server + +```bash +# Move into your project +cd my-webf-app + +# Install dependencies +npm install + +# Start the dev server +npm run dev +``` + +Your terminal will show URLs like: +``` + VITE v5.0.0 ready in 123 ms + + ➜ Local: http://localhost:5173/ + ➜ Network: http://192.168.1.100:5173/ +``` + +### 4. Load in WebF Go + +**For Desktop:** +1. Copy the `http://localhost:5173/` URL +2. Paste into WebF Go's input field +3. Press Enter or click "Go" + +**For Mobile Device:** +⚠️ **IMPORTANT**: Mobile devices cannot access `localhost` + +You MUST use the Network URL instead: +1. Make sure your computer and mobile device are on the **same WiFi network** +2. Use `--host` flag to expose the dev server: + ```bash + npm run dev -- --host + ``` +3. Copy the **Network** URL (e.g., `http://192.168.1.100:5173/`) +4. Type it into WebF Go on your mobile device +5. Press "Go" + +Your app will now render in WebF! 🎉 + +### 5. Verify Hot Reload + +Make a quick change to your code and save. The app should automatically update - this is Vite's Hot Module Replacement (HMR) working with WebF. + +### 6. (Optional) Setup Chrome DevTools + +To debug your app: +1. Click the floating debug button in WebF Go +2. Click "Copy" to get the DevTools URL (`devtools://...`) +3. Paste into desktop Google Chrome browser +4. You can now use Console, Elements, Network tabs + +**Note**: JavaScript breakpoints don't work yet - use `console.log()` instead. + +## Common Issues and Solutions + +### Issue: "Cannot connect" on mobile device + +**Causes & Solutions:** +- ✗ Using `localhost` → ✓ Use Network URL (`http://192.168.x.x:5173`) +- ✗ Different WiFi networks → ✓ Put both devices on same network +- ✗ Missing `--host` flag → ✓ Use `npm run dev -- --host` +- ✗ Firewall blocking port → ✓ Allow port 5173 through firewall + +### Issue: "Connection refused" + +- Dev server not running → Run `npm run dev` +- Wrong port number → Check terminal output for correct port +- Firewall blocking → Temporarily disable to test + +### Issue: App loads but doesn't update + +- HMR not working → Refresh the page manually +- Dev server error → Check terminal for errors +- Network connection lost → Reconnect WiFi + +## Production Deployment + +⚠️ **WebF Go is NOT for production use**. It's a testing tool for developers. + +### For Production Apps + +When you're ready to deploy to end users, you need to: + +**1. Build Your Web Bundle** +```bash +npm run build +``` + +**2. Host Your Bundle** +- Deploy to any web hosting (Vercel, Netlify, CDN, etc.) +- Your bundle will be accessible via URL (e.g., `https://your-app.com`) + +**3. Create a Flutter App with WebF Integration** + +You or your Flutter team needs to: +- Set up a Flutter project +- Add the WebF Flutter package to `pubspec.yaml` +- Configure the app (name, icon, splash screen, permissions) +- Load your web bundle URL in the WebF widget + +**Example Flutter Integration:** +```dart +import 'package:webf/webf.dart'; + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return WebF( + bundle: WebFBundle.fromUrl('https://your-app.com'), + ); + } +} +``` + +**4. Build and Deploy Flutter App** +- Build for iOS and Android +- Submit to App Store and Google Play + +**Resources:** +- [WebF Integration Guide](https://openwebf.com/en/docs/developer-guide/integration) +- [Flutter App Setup](https://openwebf.com/en/docs/developer-guide/app-setup) + +### Development vs Production + +| Aspect | Development | Production | +|--------|------------|------------| +| **Tool** | WebF Go | Custom Flutter app | +| **Purpose** | Testing & iteration | End-user distribution | +| **Setup** | Download and run | Build Flutter app | +| **Distribution** | Don't distribute | App Store/Google Play | +| **Requirements** | Node.js only | Flutter SDK required | + +## Next Steps + +Now that you have a working dev environment: + +1. **Learn the #1 difference**: WebF uses async rendering - see the `webf-async-rendering` skill +2. **Check API compatibility**: Not all web APIs work in WebF - see `webf-api-compatibility` skill +3. **Add navigation**: Multi-screen apps use WebF routing - see `webf-routing-setup` skill + +## Quick Reference + +```bash +# Create new project +npm create vite@latest my-app + +# Start dev server (desktop) +npm run dev + +# Start dev server (mobile - with network access) +npm run dev -- --host + +# Install dependencies +npm install + +# Build for production +npm run build +``` + +## Resources + +- **Getting Started Guide**: https://openwebf.com/en/docs/developer-guide/getting-started +- **WebF Go Guide**: https://openwebf.com/en/docs/learn-webf/webf-go +- **Development Workflow**: https://openwebf.com/en/docs/developer-guide/development-workflow +- **Download WebF Go**: https://openwebf.com/en/go +- **Full Documentation**: https://openwebf.com/en/docs \ No newline at end of file diff --git a/.agents/skills/webf-quickstart/reference.md b/.agents/skills/webf-quickstart/reference.md new file mode 100644 index 0000000000..df0128d8d2 --- /dev/null +++ b/.agents/skills/webf-quickstart/reference.md @@ -0,0 +1,199 @@ +# WebF Quickstart - Quick Reference + +> **⚠️ IMPORTANT**: WebF Go is for **testing and development ONLY**. For production, you must build a Flutter app with WebF integration. + +## Setup Checklist + +- [ ] Node.js installed (LTS version) +- [ ] WebF Go downloaded and installed (for testing) +- [ ] Project created with Vite +- [ ] Dependencies installed (`npm install`) +- [ ] Dev server running (`npm run dev`) +- [ ] App loaded in WebF Go + +## Essential Commands + +```bash +# Create project +npm create vite@latest my-app +cd my-app +npm install + +# Development +npm run dev # Desktop (localhost) +npm run dev -- --host # Mobile (network access) + +# Production (Web Bundle) +npm run build # Create production build +npm run preview # Preview production build +vercel deploy # Deploy to hosting +``` + +## Network URLs by Platform + +| Platform | URL Pattern | Example | +|----------|-------------|---------| +| Desktop | `http://localhost:PORT` | `http://localhost:5173` | +| Mobile | `http://NETWORK-IP:PORT` | `http://192.168.1.100:5173` | + +**Critical**: Mobile devices cannot access `localhost` - always use Network URL! + +## Troubleshooting Quick Fixes + +| Problem | Quick Fix | +|---------|-----------| +| Mobile can't connect | Use `npm run dev -- --host` and Network URL | +| Different WiFi | Put both devices on same network | +| Firewall blocks | Allow port 5173 (or your dev server port) | +| HMR not working | Hard refresh or restart dev server | +| DevTools won't connect | Must use desktop Chrome browser | + +## Port Configuration + +Default Vite port: `5173` + +To change port: +```bash +# Option 1: Flag +npm run dev -- --port 3000 + +# Option 2: vite.config.js +export default { + server: { + port: 3000, + host: true // Expose on network + } +} +``` + +## DevTools Setup + +1. Click floating button in WebF Go +2. Click "Copy" → Gets `devtools://...` URL +3. Paste in desktop Chrome +4. Use Console, Elements, Network tabs +5. ⚠️ Breakpoints don't work - use `console.log()` + +## Supported Frameworks + +All work out-of-the-box with Vite: + +- ✅ React (16, 17, 18, 19) +- ✅ Vue (2, 3) +- ✅ Svelte +- ✅ Preact +- ✅ Solid +- ✅ Qwik +- ✅ Vanilla JS + +## HTTPS for Mobile Testing + +Some APIs require HTTPS. To enable: + +```bash +npm install -D @vitejs/plugin-basic-ssl +``` + +```js +// vite.config.js +import basicSsl from '@vitejs/plugin-basic-ssl' + +export default { + plugins: [basicSsl()], + server: { + https: true + } +} +``` + +Then use `https://192.168.x.x:5173` instead of `http://` + +## Common Vite Flags + +```bash +# Network access (mobile testing) +npm run dev -- --host + +# Custom port +npm run dev -- --port 3000 + +# Open browser automatically +npm run dev -- --open + +# Clear cache +npm run dev -- --force +``` + +## File Structure + +``` +my-webf-app/ +├── src/ +│ ├── main.jsx # Entry point +│ ├── App.jsx # Root component +│ └── index.css # Global styles +├── public/ # Static assets +├── index.html # HTML template +├── package.json # Dependencies +└── vite.config.js # Vite configuration +``` + +## Environment Detection + +Check if running in WebF: + +```javascript +// WebF exposes global WebF object +if (typeof WebF !== 'undefined') { + console.log('Running in WebF'); +} else { + console.log('Running in browser'); +} +``` + +## Production Deployment + +⚠️ **WebF Go is NOT for production**. It's a testing tool only. + +### Production Checklist + +1. **Build Web Bundle** + ```bash + npm run build + ``` + +2. **Deploy Bundle to Hosting** + ```bash + vercel deploy # or Netlify, AWS S3, etc. + ``` + +3. **Create Flutter App with WebF** + - Set up Flutter project + - Add `webf` package to `pubspec.yaml` + - Configure app (icons, permissions, etc.) + - Load your bundle URL + +4. **Build & Submit Flutter App** + - Build for iOS and Android + - Submit to App Store/Google Play + +### Key Differences + +| Development | Production | +|------------|------------| +| WebF Go | Custom Flutter app | +| Testing only | End-user distribution | +| No Flutter SDK needed | Flutter SDK required | +| Don't distribute | App Store/Google Play | + +**Resources:** +- Integration: https://openwebf.com/en/docs/developer-guide/integration +- Flutter Setup: https://openwebf.com/en/docs/developer-guide/app-setup + +## Next Skill After Setup + +After successful setup, you'll need: + +1. **Async Rendering** (`webf-async-rendering`) - Most important concept +2. **API Compatibility** (`webf-api-compatibility`) - What works/doesn't work +3. **Routing Setup** (`webf-routing-setup`) - Multi-screen navigation \ No newline at end of file diff --git a/.agents/skills/webf-routing-setup/SKILL.md b/.agents/skills/webf-routing-setup/SKILL.md new file mode 100644 index 0000000000..2d27cea8bb --- /dev/null +++ b/.agents/skills/webf-routing-setup/SKILL.md @@ -0,0 +1,584 @@ +--- +name: webf-routing-setup +description: Setup hybrid routing with native screen transitions in WebF - configure navigation using WebF routing instead of SPA routing. Use when setting up navigation, implementing multi-screen apps, or when react-router-dom/vue-router doesn't work as expected. +--- + +# WebF Routing Setup + +> **Note**: WebF development is nearly identical to web development - you use the same tools (Vite, npm, Vitest), same frameworks (React, Vue, Svelte), and same deployment services (Vercel, Netlify). This skill covers **one of the 3 key differences**: routing with native screen transitions instead of SPA routing. The other two differences are async rendering and API compatibility. + +**WebF does NOT use traditional Single-Page Application (SPA) routing.** Instead, it uses **hybrid routing** where each route renders on a separate, native Flutter screen with platform-native transitions. + +## The Fundamental Difference + +### In Browsers (SPA Routing) +Traditional web routing uses the History API or hash-based routing: + +```javascript +// Browser SPA routing (react-router-dom, vue-router) +// ❌ This pattern does NOT work in WebF +import { BrowserRouter, Routes, Route } from 'react-router-dom'; + +// Single page with client-side routing +// All routes render in the same screen +// Transitions are CSS-based +``` + +The entire app runs in one screen, and route changes are simulated with JavaScript and CSS. + +### In WebF (Hybrid Routing) +Each route is a separate Flutter screen with native transitions: + +```javascript +// WebF hybrid routing +// ✅ This pattern WORKS in WebF +import { Routes, Route, WebFRouter } from '@openwebf/react-router'; + +// Each route renders on a separate Flutter screen +// Transitions use native platform animations +// Hardware back button works correctly +``` + +**Think of it like native mobile navigation** - each route is a new screen in a navigation stack, not a section of a single web page. + +## Why Hybrid Routing? + +WebF's approach provides true native app behavior: + +1. **Native Transitions** - Platform-specific animations (Cupertino for iOS, Material for Android) +2. **Proper Lifecycle** - Each route has its own lifecycle, similar to native apps +3. **Hardware Back Button** - Android back button works correctly +4. **Memory Management** - Unused routes can be unloaded +5. **Deep Linking** - Integration with platform deep linking +6. **Synchronized Navigation** - Flutter Navigator and WebF routing stay in sync + +## React Setup + +### Installation + +```bash +npm install @openwebf/react-router +``` + +**CRITICAL**: Do NOT use `react-router-dom` - it will not work correctly in WebF. + +### Basic Route Configuration + +```jsx +import { Route, Routes } from '@openwebf/react-router'; +import { HomePage } from './pages/home'; +import { ProfilePage } from './pages/profile'; +import { SettingsPage } from './pages/settings'; + +function App() { + return ( + + {/* Each Route must have a title prop */} + } title="Home" /> + } title="Profile" /> + } title="Settings" /> + + ); +} + +export default App; +``` + +**Important**: The `title` prop appears in the native navigation bar for that screen. + +### Programmatic Navigation + +Use the `WebFRouter` object for navigation: + +```jsx +import { WebFRouter } from '@openwebf/react-router'; + +function HomePage() { + // Navigate forward (push new screen) + const goToProfile = () => { + WebFRouter.pushState({ userId: 123 }, '/profile'); + }; + + // Replace current screen (no back button) + const replaceWithSettings = () => { + WebFRouter.replaceState({}, '/settings'); + }; + + // Navigate back + const goBack = () => { + WebFRouter.back(); + }; + + // Navigate forward + const goForward = () => { + WebFRouter.forward(); + }; + + return ( +
+

Home Page

+ + + + +
+ ); +} +``` + +### Passing Data Between Routes + +Use the state parameter to pass data: + +```jsx +import { WebFRouter, useLocation } from '@openwebf/react-router'; + +// Sender component +function ProductList() { + const viewProduct = (product) => { + // Pass product data to detail screen + WebFRouter.pushState({ + productId: product.id, + productName: product.name, + productPrice: product.price + }, '/product/detail'); + }; + + return ( +
+ +
+ ); +} + +// Receiver component +function ProductDetail() { + const location = useLocation(); + const { productId, productName, productPrice } = location.state || {}; + + if (!productId) { + return
No product data
; + } + + return ( +
+

{productName}

+

Price: ${productPrice}

+

ID: {productId}

+
+ ); +} +``` + +### Using Route Parameters + +WebF supports dynamic route parameters: + +```jsx +import { Route, Routes, useParams } from '@openwebf/react-router'; + +function App() { + return ( + + } title="Home" /> + } title="User Profile" /> + } title="Comment" /> + + ); +} + +function UserProfile() { + const { userId } = useParams(); + + return ( +
+

User Profile

+

User ID: {userId}

+
+ ); +} + +function CommentDetail() { + const { postId, commentId } = useParams(); + + return ( +
+

Comment Detail

+

Post ID: {postId}

+

Comment ID: {commentId}

+
+ ); +} +``` + +### Declarative Navigation with Links + +Use `WebFRouterLink` for clickable navigation: + +```jsx +import { WebFRouterLink } from '@openwebf/react-router'; + +function NavigationMenu() { + return ( + + ); +} +``` + +### Advanced Navigation Methods + +WebFRouter provides Flutter-style navigation for complex scenarios: + +```jsx +import { WebFRouter } from '@openwebf/react-router'; + +// Push a route (async, returns when screen is pushed) +await WebFRouter.push('/details', { itemId: 42 }); + +// Replace current route (no back button) +await WebFRouter.replace('/login', { sessionExpired: true }); + +// Pop and push (remove current, add new) +await WebFRouter.popAndPushNamed('/success', { orderId: 'ORD-123' }); + +// Check if can pop +if (WebFRouter.canPop()) { + const didPop = WebFRouter.maybePop({ cancelled: false }); + console.log('Did pop:', didPop); +} + +// Restorable navigation (state restoration support) +const restorationId = await WebFRouter.restorablePopAndPushNamed('/checkout', { + cartItems: items, + timestamp: Date.now() +}); +``` + +## Hooks API + +WebF routing provides React hooks for accessing route information: + +```jsx +import { useLocation, useParams, useNavigate } from '@openwebf/react-router'; + +function MyComponent() { + // Get current location (pathname, state, etc.) + const location = useLocation(); + console.log('Current path:', location.pathname); + console.log('Route state:', location.state); + + // Get route parameters + const { userId, postId } = useParams(); + + // Get navigation function + const navigate = useNavigate(); + + const handleClick = () => { + // Navigate programmatically + navigate('/profile', { userId: 123 }); + }; + + return ; +} +``` + +## Common Patterns + +### Pattern 1: Protected Routes + +Redirect to login if not authenticated: + +```jsx +import { useEffect } from 'react'; +import { WebFRouter, useLocation } from '@openwebf/react-router'; + +function ProtectedRoute({ children, isAuthenticated }) { + const location = useLocation(); + + useEffect(() => { + if (!isAuthenticated) { + // Redirect to login, save current path + WebFRouter.pushState({ + redirectTo: location.pathname + }, '/login'); + } + }, [isAuthenticated, location.pathname]); + + if (!isAuthenticated) { + return null; // Or loading spinner + } + + return children; +} + +// Usage +function App() { + const [isAuthenticated, setIsAuthenticated] = useState(false); + + return ( + + } title="Login" /> + + + + } + title="Dashboard" + /> + + ); +} +``` + +### Pattern 2: Redirecting After Login + +After successful login, navigate to saved location: + +```jsx +function LoginPage() { + const location = useLocation(); + const redirectTo = location.state?.redirectTo || '/'; + + const handleLogin = async () => { + // Perform login + await loginUser(); + + // Redirect to saved location or home + WebFRouter.replaceState({}, redirectTo); + }; + + return ( + + ); +} +``` + +### Pattern 3: Conditional Navigation + +Navigate based on result: + +```jsx +async function handleSubmit(formData) { + try { + const result = await submitForm(formData); + + if (result.success) { + // Navigate to success page + WebFRouter.pushState({ + message: result.message, + orderId: result.orderId + }, '/success'); + } else { + // Navigate to error page + WebFRouter.pushState({ + error: result.error + }, '/error'); + } + } catch (error) { + // Handle error + WebFRouter.pushState({ + error: error.message + }, '/error'); + } +} +``` + +### Pattern 4: Preventing Navigation + +Confirm before leaving unsaved changes: + +```jsx +import { useEffect } from 'react'; +import { WebFRouter } from '@openwebf/react-router'; + +function FormPage() { + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + + useEffect(() => { + if (!hasUnsavedChanges) return; + + // Custom back button handler + const handleBack = () => { + const shouldLeave = confirm('You have unsaved changes. Leave anyway?'); + if (shouldLeave) { + setHasUnsavedChanges(false); + WebFRouter.back(); + } + }; + + // Note: This is a simplified example + // Actual implementation depends on your back button handling + }, [hasUnsavedChanges]); + + return ( +
setHasUnsavedChanges(true)}> + {/* Form fields */} +
+ ); +} +``` + +## Vue Setup + +For Vue applications, use `@openwebf/vue-router`: + +```bash +npm install @openwebf/vue-router +``` + +**Note**: The API is similar to Vue Router but adapted for WebF's hybrid routing. Full Vue examples are available in `examples.md`. + +## Cross-Platform Support + +For apps that run in both WebF and browsers, see `cross-platform.md` for router adapter patterns. + +## Key Differences from SPA Routing + +| Feature | SPA Routing (Browser) | Hybrid Routing (WebF) | +|---------|----------------------|----------------------| +| Screen transitions | CSS animations | Native platform animations | +| Route lifecycle | JavaScript-managed | Flutter-managed | +| Memory management | Manual | Automatic (Flutter Navigator) | +| Back button | History API | Hardware back button | +| Deep linking | URL-based | Platform deep linking | +| Route stacking | Virtual | Real native screen stack | + +## Common Mistakes + +### Mistake 1: Using react-router-dom + +```jsx +// ❌ WRONG - Will not work correctly in WebF +import { BrowserRouter, Routes, Route } from 'react-router-dom'; + +function App() { + return ( + + + } /> + + + ); +} +``` + +```jsx +// ✅ CORRECT - Use @openwebf/react-router +import { Routes, Route } from '@openwebf/react-router'; + +function App() { + return ( + + } title="Home" /> + + ); +} +``` + +### Mistake 2: Forgetting title Prop + +```jsx +// ❌ WRONG - Missing title prop +} /> + +// ✅ CORRECT - Include title +} title="Home" /> +``` + +### Mistake 3: Using window.history + +```javascript +// ❌ WRONG - History API doesn't work in WebF +window.history.pushState({}, '', '/new-path'); + +// ✅ CORRECT - Use WebFRouter +WebFRouter.pushState({}, '/new-path'); +``` + +### Mistake 4: Expecting SPA Behavior + +```jsx +// ❌ WRONG - Expecting all routes to share state +// In WebF, each route is a separate screen +const [sharedState, setSharedState] = useState({}); // Won't persist across routes + +// ✅ CORRECT - Use proper state management +// Use Context, Redux, or pass data via route state +WebFRouter.pushState({ data: myData }, '/next-route'); +``` + +## Resources + +- **Routing Documentation**: https://openwebf.com/en/docs/developer-guide/routing +- **Core Concepts - Hybrid Routing**: https://openwebf.com/en/docs/developer-guide/core-concepts#hybrid-routing +- **Complete Examples**: See `examples.md` in this skill +- **Cross-Platform Patterns**: See `cross-platform.md` in this skill +- **npm Package**: https://www.npmjs.com/package/@openwebf/react-router + +## Quick Reference + +```bash +# Install React router +npm install @openwebf/react-router + +# Install Vue router +npm install @openwebf/vue-router +``` + +```jsx +// Basic setup +import { Routes, Route, WebFRouter } from '@openwebf/react-router'; + +// Navigate forward +WebFRouter.pushState({ data }, '/path'); + +// Navigate back +WebFRouter.back(); + +// Replace current +WebFRouter.replaceState({ data }, '/path'); + +// Get location +const location = useLocation(); + +// Get params +const { id } = useParams(); +``` + +## Key Takeaways + +✅ **DO**: +- Use `@openwebf/react-router` or `@openwebf/vue-router` +- Include `title` prop on all routes +- Use `WebFRouter` for navigation +- Pass data via route state +- Think of routes as native screens + +❌ **DON'T**: +- Use react-router-dom or vue-router directly +- Expect SPA routing behavior +- Use window.history API +- Share state across routes without proper state management +- Forget that each route is a separate Flutter screen \ No newline at end of file diff --git a/.agents/skills/webf-routing-setup/cross-platform.md b/.agents/skills/webf-routing-setup/cross-platform.md new file mode 100644 index 0000000000..fea07169b8 --- /dev/null +++ b/.agents/skills/webf-routing-setup/cross-platform.md @@ -0,0 +1,528 @@ +# Cross-Platform Routing Patterns + +Build applications that work in both WebF (native) and browsers (web) using a single codebase. + +## The Challenge + +WebF uses hybrid routing with native screen transitions: +- `@openwebf/react-router` for WebF +- Each route is a separate Flutter screen + +Browsers use SPA routing with History API: +- `react-router-dom` for browsers +- All routes render in a single page +- Transitions are CSS-based + +**Goal**: Write one app that works in both environments. + +## Solution: Router Adapter Pattern + +Create an adapter that detects the environment and uses the appropriate router. + +## Complete React Example + +### Step 1: Install Both Routers + +```bash +# WebF router +npm install @openwebf/react-router + +# Browser router +npm install react-router-dom +``` + +### Step 2: Create Router Adapter + +```jsx +// src/routing/router-adapter.jsx +import * as WebFRouter from '@openwebf/react-router'; +import * as BrowserRouter from 'react-router-dom'; + +// Detect if running in WebF +const isWebF = typeof window !== 'undefined' && typeof (window as any).webf !== 'undefined'; + +// Export appropriate router components +export const Routes = isWebF ? WebFRouter.Routes : BrowserRouter.Routes; +export const Route = isWebF ? WebFRouter.Route : BrowserRouter.Route; +export const useLocation = isWebF ? WebFRouter.useLocation : BrowserRouter.useLocation; +export const useParams = isWebF ? WebFRouter.useParams : BrowserRouter.useParams; +export const useNavigate = isWebF ? WebFRouter.useNavigate : BrowserRouter.useNavigate; + +// Router provider wrapper +export function RouterProvider({ children }) { + if (isWebF) { + // WebF doesn't need a provider wrapper + return <>{children}; + } + + // Browser needs BrowserRouter wrapper + return {children}; +} + +// Navigation helper (unified API) +export const navigate = { + push: (path, state = {}) => { + if (isWebF) { + WebFRouter.WebFRouter.pushState(state, path); + } else { + // In browser, use window.history or useNavigate hook + window.history.pushState(state, '', path); + window.dispatchEvent(new PopStateEvent('popstate')); + } + }, + + replace: (path, state = {}) => { + if (isWebF) { + WebFRouter.WebFRouter.replaceState(state, path); + } else { + window.history.replaceState(state, '', path); + window.dispatchEvent(new PopStateEvent('popstate')); + } + }, + + back: () => { + if (isWebF) { + WebFRouter.WebFRouter.back(); + } else { + window.history.back(); + } + }, + + forward: () => { + if (isWebF) { + WebFRouter.WebFRouter.forward(); + } else { + window.history.forward(); + } + } +}; + +// Link component that works in both environments +export function Link({ to, state, children, ...props }) { + const handleClick = (e) => { + e.preventDefault(); + navigate.push(to, state); + }; + + return ( + + {children} + + ); +} + +// Check if running in WebF +export { isWebF }; +``` + +### Step 3: Update App to Use Adapter + +```jsx +// src/App.jsx +import { Routes, Route, RouterProvider } from './routing/router-adapter'; +import HomePage from './pages/HomePage'; +import ProfilePage from './pages/ProfilePage'; +import SettingsPage from './pages/SettingsPage'; + +function App() { + return ( + + + } title="Home" /> + } title="Profile" /> + } title="Settings" /> + + + ); +} + +export default App; +``` + +**Note**: The `title` prop is ignored by react-router-dom, so it's safe to include for both. + +### Step 4: Use Adapter in Components + +```jsx +// src/pages/HomePage.jsx +import { navigate, Link, isWebF } from '../routing/router-adapter'; + +function HomePage() { + const goToProfile = () => { + navigate.push('/profile', { source: 'home' }); + }; + + return ( +
+

Home Page

+ + {isWebF && ( +

+ Running in WebF (Native App) +

+ )} + + {!isWebF && ( +

+ Running in Browser (Web) +

+ )} + +
+ {/* Programmatic navigation */} + + + {/* Declarative navigation */} + + Go to Settings (Link) + +
+
+ ); +} + +export default HomePage; +``` + +```jsx +// src/pages/ProfilePage.jsx +import { useLocation, navigate } from '../routing/router-adapter'; + +function ProfilePage() { + const location = useLocation(); + const source = location.state?.source; + + const goBack = () => { + navigate.back(); + }; + + return ( +
+

Profile Page

+ + {source && ( +

Came from: {source}

+ )} + + +
+ ); +} + +export default ProfilePage; +``` + +## Advanced: Custom Hook for Navigation + +Create a unified `useNavigation` hook: + +```jsx +// src/routing/use-navigation.js +import { useCallback } from 'react'; +import { navigate, isWebF } from './router-adapter'; + +export function useNavigation() { + const push = useCallback((path, state) => { + navigate.push(path, state); + }, []); + + const replace = useCallback((path, state) => { + navigate.replace(path, state); + }, []); + + const back = useCallback(() => { + navigate.back(); + }, []); + + const forward = useCallback(() => { + navigate.forward(); + }, []); + + return { + push, + replace, + back, + forward, + isWebF + }; +} +``` + +**Usage**: + +```jsx +import { useNavigation } from '../routing/use-navigation'; + +function MyComponent() { + const { push, back, isWebF } = useNavigation(); + + const handleClick = () => { + push('/details', { itemId: 123 }); + }; + + return ( +
+ + + + {isWebF ? ( +

Native transitions enabled

+ ) : ( +

Browser mode

+ )} +
+ ); +} +``` + +## Environment Detection + +### Method 1: Check for WebF Global + +```javascript +const isWebF = typeof window !== 'undefined' && typeof window.webf !== 'undefined'; +``` + +### Method 2: Check for WebFRouter + +```javascript +import { WebFRouter } from '@openwebf/react-router'; + +const isWebF = typeof WebFRouter !== 'undefined'; +``` + +### Method 3: Environment Variable + +Set environment variable during build: + +```bash +# .env.webf +VITE_PLATFORM=webf + +# .env.browser +VITE_PLATFORM=browser +``` + +```javascript +const isWebF = import.meta.env.VITE_PLATFORM === 'webf'; +``` + +## Platform-Specific Features + +Some features only make sense in one environment: + +```jsx +import { isWebF } from './routing/router-adapter'; +import { WebFShare } from '@openwebf/webf-share'; + +function ShareButton({ title, url }) { + if (!isWebF) { + // Browser: Use Web Share API or fallback + const handleShare = async () => { + if (navigator.share) { + await navigator.share({ title, url }); + } else { + // Fallback: copy to clipboard + navigator.clipboard.writeText(url); + alert('Link copied to clipboard!'); + } + }; + + return ; + } + + // WebF: Use native share + if (!WebFShare.isAvailable()) { + return null; + } + + const handleShare = async () => { + await WebFShare.shareText({ text: title, url }); + }; + + return ; +} +``` + +## Build Configuration + +### Vite Configuration + +```javascript +// vite.config.js +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig(({ mode }) => { + const isWebF = mode === 'webf'; + + return { + plugins: [react()], + define: { + 'import.meta.env.IS_WEBF': isWebF + }, + build: { + // Different output directories for different platforms + outDir: isWebF ? 'dist-webf' : 'dist-browser' + } + }; +}); +``` + +**Build commands**: + +```bash +# Build for browser +npm run build + +# Build for WebF +npm run build -- --mode webf +``` + +### Package.json Scripts + +```json +{ + "scripts": { + "dev": "vite", + "dev:webf": "vite --mode webf", + "build": "vite build", + "build:webf": "vite build --mode webf", + "preview": "vite preview" + } +} +``` + +## TypeScript Support + +Add types for the router adapter: + +```typescript +// src/routing/router-adapter.d.ts +export interface NavigateState { + [key: string]: any; +} + +export interface NavigateAPI { + push: (path: string, state?: NavigateState) => void; + replace: (path: string, state?: NavigateState) => void; + back: () => void; + forward: () => void; +} + +export const navigate: NavigateAPI; +export const isWebF: boolean; + +export function RouterProvider({ children }: { children: React.ReactNode }): JSX.Element; +export function Link({ to, state, children, ...props }: { + to: string; + state?: NavigateState; + children: React.ReactNode; + [key: string]: any; +}): JSX.Element; + +export { Routes, Route, useLocation, useParams, useNavigate } from '@openwebf/react-router'; +``` + +## Testing Both Platforms + +### Test in Browser + +```bash +npm run dev +# Open http://localhost:5173 in browser +``` + +### Test in WebF + +```bash +npm run dev -- --host +# Open http://192.168.x.x:5173 in WebF Go +``` + +## Best Practices + +### 1. Use Adapter Consistently + +❌ **Don't mix routers**: +```jsx +import { WebFRouter } from '@openwebf/react-router'; +import { useNavigate } from 'react-router-dom'; +// This will break! +``` + +✅ **Use adapter everywhere**: +```jsx +import { navigate, useLocation } from './routing/router-adapter'; +// Works in both environments +``` + +### 2. Handle Missing State Gracefully + +```jsx +function DetailPage() { + const location = useLocation(); + const data = location.state?.data || getDefaultData(); + + // Always have fallback for missing state + return
{data.name}
; +} +``` + +### 3. Test in Both Environments + +- Develop in browser for fast iteration +- Test in WebF Go regularly +- CI/CD should test both builds + +### 4. Platform-Specific Code + +Use feature flags for platform-specific behavior: + +```jsx +import { isWebF } from './routing/router-adapter'; + +function Header() { + return ( +
+

My App

+ {isWebF ? ( + + ) : ( + + )} +
+ ); +} +``` + +### 5. Keep Router Logic Simple + +Don't create overly complex abstractions. The adapter should be thin and predictable. + +## Summary + +The router adapter pattern allows you to: + +1. ✅ Write once, run in browser and WebF +2. ✅ Use familiar routing APIs +3. ✅ Handle platform differences gracefully +4. ✅ Test consistently across platforms +5. ✅ Maintain a single codebase + +**Key Files**: +- `src/routing/router-adapter.jsx` - Router abstraction +- `src/routing/use-navigation.js` - Navigation hook +- `.env.webf` / `.env.browser` - Environment config + +**Development Flow**: +1. Develop in browser (fast iteration) +2. Test in WebF Go (native behavior) +3. Build for both platforms +4. Deploy to web and app stores + +This approach gives you the best of both worlds: web development speed and native app experience. \ No newline at end of file diff --git a/.agents/skills/webf-routing-setup/examples.md b/.agents/skills/webf-routing-setup/examples.md new file mode 100644 index 0000000000..e514714829 --- /dev/null +++ b/.agents/skills/webf-routing-setup/examples.md @@ -0,0 +1,947 @@ +# WebF Routing - Complete Examples + +## Example 1: Basic Multi-Screen App (React) + +A simple app with three screens: Home, Profile, and Settings. + +```jsx +// src/App.jsx +import { Routes, Route } from '@openwebf/react-router'; +import HomePage from './pages/HomePage'; +import ProfilePage from './pages/ProfilePage'; +import SettingsPage from './pages/SettingsPage'; + +function App() { + return ( + + } title="Home" /> + } title="Profile" /> + } title="Settings" /> + + ); +} + +export default App; +``` + +```jsx +// src/pages/HomePage.jsx +import { WebFRouter } from '@openwebf/react-router'; + +function HomePage() { + const goToProfile = () => { + WebFRouter.pushState({}, '/profile'); + }; + + const goToSettings = () => { + WebFRouter.pushState({}, '/settings'); + }; + + return ( +
+

Home Page

+

Welcome to the home page!

+ +
+ + +
+
+ ); +} + +export default HomePage; +``` + +```jsx +// src/pages/ProfilePage.jsx +import { WebFRouter } from '@openwebf/react-router'; + +function ProfilePage() { + const goBack = () => { + WebFRouter.back(); + }; + + return ( +
+

Profile Page

+

This is your profile.

+ + +
+ ); +} + +export default ProfilePage; +``` + +```jsx +// src/pages/SettingsPage.jsx +import { WebFRouter } from '@openwebf/react-router'; + +function SettingsPage() { + const goHome = () => { + // Replace current screen with home (no back button) + WebFRouter.replaceState({}, '/'); + }; + + return ( +
+

Settings

+

App settings go here.

+ + +
+ ); +} + +export default SettingsPage; +``` + +## Example 2: Passing Data Between Screens + +Shopping app with product list and detail screens. + +```jsx +// src/App.jsx +import { Routes, Route } from '@openwebf/react-router'; +import ProductListPage from './pages/ProductListPage'; +import ProductDetailPage from './pages/ProductDetailPage'; +import CartPage from './pages/CartPage'; + +function App() { + return ( + + } title="Products" /> + } title="Product Detail" /> + } title="Shopping Cart" /> + + ); +} + +export default App; +``` + +```jsx +// src/pages/ProductListPage.jsx +import { WebFRouter } from '@openwebf/react-router'; + +const PRODUCTS = [ + { id: 1, name: 'Laptop', price: 999.99, description: 'Powerful laptop for work' }, + { id: 2, name: 'Mouse', price: 29.99, description: 'Wireless mouse' }, + { id: 3, name: 'Keyboard', price: 79.99, description: 'Mechanical keyboard' } +]; + +function ProductListPage() { + const viewProduct = (product) => { + // Pass entire product object to detail screen + WebFRouter.pushState({ + product: product, + source: 'list' + }, '/product/detail'); + }; + + const goToCart = () => { + WebFRouter.pushState({}, '/cart'); + }; + + return ( +
+

Products

+ +
+ +
+ +
+ {PRODUCTS.map(product => ( +
+

{product.name}

+

${product.price}

+ +
+ ))} +
+
+ ); +} + +export default ProductListPage; +``` + +```jsx +// src/pages/ProductDetailPage.jsx +import { useState } from 'react'; +import { WebFRouter, useLocation } from '@openwebf/react-router'; + +function ProductDetailPage() { + const location = useLocation(); + const { product, source } = location.state || {}; + const [quantity, setQuantity] = useState(1); + + if (!product) { + return ( +
+

No product data found.

+ +
+ ); + } + + const addToCart = () => { + // In a real app, you'd add to cart state/context + console.log(`Added ${quantity} x ${product.name} to cart`); + + // Navigate to cart with confirmation + WebFRouter.pushState({ + addedProduct: product, + addedQuantity: quantity + }, '/cart'); + }; + + const goBack = () => { + WebFRouter.back(); + }; + + return ( +
+

{product.name}

+

+ ${product.price} +

+

{product.description}

+ +
+ +
+ +
+ + +
+ + {source && ( +

+ Came from: {source} +

+ )} +
+ ); +} + +export default ProductDetailPage; +``` + +```jsx +// src/pages/CartPage.jsx +import { useLocation, WebFRouter } from '@openwebf/react-router'; + +function CartPage() { + const location = useLocation(); + const { addedProduct, addedQuantity } = location.state || {}; + + const goBack = () => { + WebFRouter.back(); + }; + + const continueShopping = () => { + // Go back to product list (could use replaceState to prevent back navigation) + WebFRouter.pushState({}, '/'); + }; + + return ( +
+

Shopping Cart

+ + {addedProduct && ( +
+ ✓ Added to cart: {addedQuantity} x {addedProduct.name} +
+ )} + +

Your cart items would appear here.

+ +
+ + +
+
+ ); +} + +export default CartPage; +``` + +## Example 3: Dynamic Routes with Parameters + +Blog app with dynamic post IDs. + +```jsx +// src/App.jsx +import { Routes, Route } from '@openwebf/react-router'; +import BlogListPage from './pages/BlogListPage'; +import BlogPostPage from './pages/BlogPostPage'; +import AuthorPage from './pages/AuthorPage'; + +function App() { + return ( + + } title="Blog" /> + } title="Post" /> + } title="Author" /> + + ); +} + +export default App; +``` + +```jsx +// src/pages/BlogListPage.jsx +import { WebFRouter } from '@openwebf/react-router'; + +const POSTS = [ + { id: '1', title: 'Getting Started with WebF', authorId: 'alice' }, + { id: '2', title: 'Understanding Async Rendering', authorId: 'bob' }, + { id: '3', title: 'Hybrid Routing Explained', authorId: 'alice' } +]; + +function BlogListPage() { + const viewPost = (postId) => { + // Navigate using route parameter + WebFRouter.pushState({}, `/post/${postId}`); + }; + + return ( +
+

Blog Posts

+ +
+ {POSTS.map(post => ( +
+

{post.title}

+

By: {post.authorId}

+ +
+ ))} +
+
+ ); +} + +export default BlogListPage; +``` + +```jsx +// src/pages/BlogPostPage.jsx +import { useParams, WebFRouter } from '@openwebf/react-router'; + +const POSTS = { + '1': { + id: '1', + title: 'Getting Started with WebF', + content: 'WebF is a W3C/WHATWG-compliant web runtime for Flutter...', + authorId: 'alice', + authorName: 'Alice Smith' + }, + '2': { + id: '2', + title: 'Understanding Async Rendering', + content: 'WebF uses async rendering which is different from browsers...', + authorId: 'bob', + authorName: 'Bob Johnson' + }, + '3': { + id: '3', + title: 'Hybrid Routing Explained', + content: 'Each route in WebF is a separate Flutter screen...', + authorId: 'alice', + authorName: 'Alice Smith' + } +}; + +function BlogPostPage() { + // Extract postId from URL parameter + const { postId } = useParams(); + const post = POSTS[postId]; + + if (!post) { + return ( +
+

Post Not Found

+ +
+ ); + } + + const viewAuthor = () => { + WebFRouter.pushState( + { authorName: post.authorName }, + `/author/${post.authorId}` + ); + }; + + const goBack = () => { + WebFRouter.back(); + }; + + return ( +
+

{post.title}

+ +

+ By +

+ +
+ {post.content} +
+ + +
+ ); +} + +export default BlogPostPage; +``` + +```jsx +// src/pages/AuthorPage.jsx +import { useParams, useLocation, WebFRouter } from '@openwebf/react-router'; + +function AuthorPage() { + const { authorId } = useParams(); + const location = useLocation(); + const { authorName } = location.state || {}; + + const goBack = () => { + WebFRouter.back(); + }; + + return ( +
+

Author: {authorName || authorId}

+

Author ID: {authorId}

+

This would show the author's profile and posts.

+ + +
+ ); +} + +export default AuthorPage; +``` + +## Example 4: Authentication Flow + +Login flow with protected routes and redirect after authentication. + +```jsx +// src/App.jsx +import { Routes, Route } from '@openwebf/react-router'; +import { useState } from 'react'; +import LoginPage from './pages/LoginPage'; +import DashboardPage from './pages/DashboardPage'; +import ProfilePage from './pages/ProfilePage'; +import ProtectedRoute from './components/ProtectedRoute'; + +function App() { + const [user, setUser] = useState(null); + + return ( + + } + title="Login" + /> + + + + } + title="Dashboard" + /> + + + + } + title="Profile" + /> + + ); +} + +export default App; +``` + +```jsx +// src/components/ProtectedRoute.jsx +import { useEffect } from 'react'; +import { WebFRouter, useLocation } from '@openwebf/react-router'; + +function ProtectedRoute({ children, user }) { + const location = useLocation(); + + useEffect(() => { + if (!user) { + // Save current path for redirect after login + WebFRouter.replaceState({ + redirectTo: location.pathname + }, '/login'); + } + }, [user, location.pathname]); + + if (!user) { + return null; // Or a loading spinner + } + + return children; +} + +export default ProtectedRoute; +``` + +```jsx +// src/pages/LoginPage.jsx +import { useState } from 'react'; +import { WebFRouter, useLocation } from '@openwebf/react-router'; + +function LoginPage({ setUser }) { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const location = useLocation(); + const redirectTo = location.state?.redirectTo || '/'; + + const handleLogin = async (e) => { + e.preventDefault(); + setError(''); + + // Simulate login + if (email && password) { + // In real app, call authentication API + const user = { + id: 1, + email: email, + name: 'John Doe' + }; + + setUser(user); + + // Redirect to saved location or home + WebFRouter.replaceState({}, redirectTo); + } else { + setError('Please enter email and password'); + } + }; + + return ( +
+

Login

+ + {redirectTo !== '/' && ( +

+ Please log in to continue to {redirectTo} +

+ )} + +
+
+ + setEmail(e.target.value)} + style={{ width: '100%', padding: '8px' }} + /> +
+ +
+ + setPassword(e.target.value)} + style={{ width: '100%', padding: '8px' }} + /> +
+ + {error && ( +

{error}

+ )} + + +
+
+ ); +} + +export default LoginPage; +``` + +```jsx +// src/pages/DashboardPage.jsx +import { WebFRouter } from '@openwebf/react-router'; + +function DashboardPage({ user }) { + const goToProfile = () => { + WebFRouter.pushState({}, '/profile'); + }; + + return ( +
+

Dashboard

+

Welcome, {user.name}!

+ +
+ +
+
+ ); +} + +export default DashboardPage; +``` + +```jsx +// src/pages/ProfilePage.jsx +import { WebFRouter } from '@openwebf/react-router'; + +function ProfilePage({ user, setUser }) { + const handleLogout = () => { + setUser(null); + WebFRouter.replaceState({}, '/login'); + }; + + const goBack = () => { + WebFRouter.back(); + }; + + return ( +
+

Profile

+

Name: {user.name}

+

Email: {user.email}

+

ID: {user.id}

+ +
+ + +
+
+ ); +} + +export default ProfilePage; +``` + +## Example 5: Tabs with Navigation + +App with tab navigation and nested routes. + +```jsx +// src/App.jsx +import { Routes, Route } from '@openwebf/react-router'; +import TabsLayout from './layouts/TabsLayout'; +import HomePage from './pages/tabs/HomePage'; +import ExplorePage from './pages/tabs/ExplorePage'; +import NotificationsPage from './pages/tabs/NotificationsPage'; +import ProfilePage from './pages/tabs/ProfilePage'; +import SettingsPage from './pages/SettingsPage'; + +function App() { + return ( + + {/* Tab routes wrapped in TabsLayout */} + } title="Home" /> + } title="Explore" /> + } title="Notifications" /> + } title="Profile" /> + + {/* Full screen routes (no tabs) */} + } title="Settings" /> + + ); +} + +export default App; +``` + +```jsx +// src/layouts/TabsLayout.jsx +import { WebFRouter, useLocation } from '@openwebf/react-router'; + +function TabsLayout({ children }) { + const location = useLocation(); + const currentPath = location.pathname; + + const tabs = [ + { path: '/', label: 'Home' }, + { path: '/explore', label: 'Explore' }, + { path: '/notifications', label: 'Notifications' }, + { path: '/profile', label: 'Profile' } + ]; + + const navigateToTab = (path) => { + if (path !== currentPath) { + WebFRouter.pushState({}, path); + } + }; + + return ( +
+ {/* Content area */} +
+ {children} +
+ + {/* Tab bar */} +
+ {tabs.map(tab => ( + + ))} +
+
+ ); +} + +export default TabsLayout; +``` + +```jsx +// src/pages/tabs/HomePage.jsx +import { WebFRouter } from '@openwebf/react-router'; + +function HomePage() { + const goToSettings = () => { + // Navigate to full-screen settings (no tabs) + WebFRouter.pushState({}, '/settings'); + }; + + return ( +
+

Home

+

Welcome to the home tab!

+ + +
+ ); +} + +export default HomePage; +``` + +```jsx +// src/pages/tabs/ProfilePage.jsx +function ProfilePage() { + return ( +
+

Profile

+

Your profile information.

+
+ ); +} + +export default ProfilePage; +``` + +```jsx +// src/pages/SettingsPage.jsx +import { WebFRouter } from '@openwebf/react-router'; + +function SettingsPage() { + const goBack = () => { + WebFRouter.back(); + }; + + return ( +
+

Settings

+

App settings (full screen, no tabs).

+ + +
+ ); +} + +export default SettingsPage; +``` + +## Key Patterns Summary + +### Pattern 1: Basic Navigation +```jsx +WebFRouter.pushState({}, '/path'); +``` + +### Pattern 2: Pass Data +```jsx +WebFRouter.pushState({ data: value }, '/path'); +``` + +### Pattern 3: Access Data +```jsx +const location = useLocation(); +const data = location.state?.data; +``` + +### Pattern 4: Route Parameters +```jsx +// Define route: /user/:userId +const { userId } = useParams(); +``` + +### Pattern 5: Replace (No Back Button) +```jsx +WebFRouter.replaceState({}, '/path'); +``` + +### Pattern 6: Go Back +```jsx +WebFRouter.back(); +``` + +### Pattern 7: Protected Routes +```jsx + + + +``` + +## Testing Navigation + +To test navigation in development: + +1. **Check route transitions**: Verify native transitions happen +2. **Test back button**: Hardware back button should work on Android +3. **Verify state passing**: Data should persist between screens +4. **Test deep linking**: Direct URLs should work +5. **Check lifecycle**: Components should mount/unmount correctly + +## Common Issues and Solutions + +### Issue: "Cannot read property 'state' of undefined" +**Cause**: Accessing `location.state` when no state was passed +**Solution**: Use optional chaining or default values +```jsx +const data = location.state?.data || defaultValue; +``` + +### Issue: Navigation doesn't work +**Cause**: Using react-router-dom instead of @openwebf/react-router +**Solution**: Install and import correct package +```bash +npm install @openwebf/react-router +``` + +### Issue: Back button goes to wrong screen +**Cause**: Using `pushState` when should use `replaceState` +**Solution**: Use `replaceState` for redirects +```jsx +// After login, replace login screen +WebFRouter.replaceState({}, '/dashboard'); +``` From 896c27c7f602821d2891a65c355dd2dc4e9e0a15 Mon Sep 17 00:00:00 2001 From: andycall Date: Thu, 26 Mar 2026 10:38:02 -0700 Subject: [PATCH 02/13] profiler: add profiler test specs. --- integration_tests/.metadata | 24 +- integration_tests/android/.gitignore | 14 + .../android/app/build.gradle.kts | 57 + .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 45 + .../webf_integration_tests/MainActivity.kt | 5 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/styles.xml | 18 + .../app/src/profile/AndroidManifest.xml | 7 + integration_tests/android/build.gradle.kts | 24 + integration_tests/android/gradle.properties | 2 + .../gradle/wrapper/gradle-wrapper.properties | 5 + integration_tests/android/settings.gradle.kts | 26 + .../profile_hotspot_cases_test.dart | 7756 +++++++++++++++-- .../custom_elements/flutter_bottom_sheet.dart | 211 + .../lib/custom_elements/main.dart | 3 + .../scripts/profile_hotspots_integration.js | 189 +- .../profile_hotspot_cases_test.dart | 174 +- webf/android/build.gradle | 9 + 26 files changed, 7974 insertions(+), 644 deletions(-) create mode 100644 integration_tests/android/.gitignore create mode 100644 integration_tests/android/app/build.gradle.kts create mode 100644 integration_tests/android/app/src/debug/AndroidManifest.xml create mode 100644 integration_tests/android/app/src/main/AndroidManifest.xml create mode 100644 integration_tests/android/app/src/main/kotlin/com/example/webf_integration_tests/MainActivity.kt create mode 100644 integration_tests/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 integration_tests/android/app/src/main/res/drawable/launch_background.xml create mode 100644 integration_tests/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 integration_tests/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 integration_tests/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 integration_tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 integration_tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 integration_tests/android/app/src/main/res/values-night/styles.xml create mode 100644 integration_tests/android/app/src/main/res/values/styles.xml create mode 100644 integration_tests/android/app/src/profile/AndroidManifest.xml create mode 100644 integration_tests/android/build.gradle.kts create mode 100644 integration_tests/android/gradle.properties create mode 100644 integration_tests/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 integration_tests/android/settings.gradle.kts create mode 100644 integration_tests/lib/custom_elements/flutter_bottom_sheet.dart diff --git a/integration_tests/.metadata b/integration_tests/.metadata index 0b0bf63d1b..22fd632353 100644 --- a/integration_tests/.metadata +++ b/integration_tests/.metadata @@ -4,7 +4,27 @@ # This file should be version controlled and should not be manually edited. version: - revision: 09126abb222d0f25b03318a1ab4a99d27d9aaa8d - channel: unknown + revision: "582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536" + channel: "[user-branch]" project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536 + base_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536 + - platform: android + create_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536 + base_revision: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/integration_tests/android/.gitignore b/integration_tests/android/.gitignore new file mode 100644 index 0000000000..be3943c96d --- /dev/null +++ b/integration_tests/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/integration_tests/android/app/build.gradle.kts b/integration_tests/android/app/build.gradle.kts new file mode 100644 index 0000000000..914f3d511e --- /dev/null +++ b/integration_tests/android/app/build.gradle.kts @@ -0,0 +1,57 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +val webfTargetAbis: List = + providers + .gradleProperty("webfTargetAbis") + .orElse("arm64-v8a") + .get() + .split(',') + .map(String::trim) + .filter(String::isNotEmpty) + +android { + namespace = "com.example.webf_integration_tests" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.webf_integration_tests" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + + ndk { + abiFilters += webfTargetAbis + } + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/integration_tests/android/app/src/debug/AndroidManifest.xml b/integration_tests/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000000..399f6981d5 --- /dev/null +++ b/integration_tests/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/integration_tests/android/app/src/main/AndroidManifest.xml b/integration_tests/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..22154ef7b9 --- /dev/null +++ b/integration_tests/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/integration_tests/android/app/src/main/kotlin/com/example/webf_integration_tests/MainActivity.kt b/integration_tests/android/app/src/main/kotlin/com/example/webf_integration_tests/MainActivity.kt new file mode 100644 index 0000000000..fbe9649fc4 --- /dev/null +++ b/integration_tests/android/app/src/main/kotlin/com/example/webf_integration_tests/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.webf_integration_tests + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/integration_tests/android/app/src/main/res/drawable-v21/launch_background.xml b/integration_tests/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000000..f74085f3f6 --- /dev/null +++ b/integration_tests/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/integration_tests/android/app/src/main/res/drawable/launch_background.xml b/integration_tests/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000000..304732f884 --- /dev/null +++ b/integration_tests/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/integration_tests/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/integration_tests/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/integration_tests/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/integration_tests/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/integration_tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/integration_tests/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/integration_tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/integration_tests/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/integration_tests/android/app/src/main/res/values-night/styles.xml b/integration_tests/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000000..06952be745 --- /dev/null +++ b/integration_tests/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/integration_tests/android/app/src/main/res/values/styles.xml b/integration_tests/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000000..cb1ef88056 --- /dev/null +++ b/integration_tests/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/integration_tests/android/app/src/profile/AndroidManifest.xml b/integration_tests/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000000..399f6981d5 --- /dev/null +++ b/integration_tests/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/integration_tests/android/build.gradle.kts b/integration_tests/android/build.gradle.kts new file mode 100644 index 0000000000..dbee657bb5 --- /dev/null +++ b/integration_tests/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/integration_tests/android/gradle.properties b/integration_tests/android/gradle.properties new file mode 100644 index 0000000000..fbee1d8cda --- /dev/null +++ b/integration_tests/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true diff --git a/integration_tests/android/gradle/wrapper/gradle-wrapper.properties b/integration_tests/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..e4ef43fb98 --- /dev/null +++ b/integration_tests/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/integration_tests/android/settings.gradle.kts b/integration_tests/android/settings.gradle.kts new file mode 100644 index 0000000000..ca7fe065c1 --- /dev/null +++ b/integration_tests/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/integration_tests/integration_test/profile_hotspot_cases_test.dart b/integration_tests/integration_test/profile_hotspot_cases_test.dart index 66a7b3ae3f..1f3ed0b00b 100644 --- a/integration_tests/integration_test/profile_hotspot_cases_test.dart +++ b/integration_tests/integration_test/profile_hotspot_cases_test.dart @@ -18,6 +18,36 @@ final developer.UserTag _paragraphRebuildProfileTag = developer.UserTag('profile_hotspots.paragraph_rebuild'); final developer.UserTag _flexInlineLayoutProfileTag = developer.UserTag('profile_hotspots.flex_inline_layout'); +final developer.UserTag _fiatFilterPopupProfileTag = + developer.UserTag('profile_hotspots.fiat_filter_popup'); +final developer.UserTag _paymentMethodSheetProfileTag = + developer.UserTag('profile_hotspots.payment_method_sheet'); +final developer.UserTag _paymentMethodBottomSheetProfileTag = + developer.UserTag('profile_hotspots.payment_method_bottom_sheet'); +final developer.UserTag _paymentMethodBottomSheetTightProfileTag = + developer.UserTag('profile_hotspots.payment_method_bottom_sheet_tight'); +final developer.UserTag _paymentMethodFastPathSheetProfileTag = + developer.UserTag('profile_hotspots.payment_method_fastpath_sheet'); +final developer.UserTag _paymentMethodPickerModalProfileTag = + developer.UserTag('profile_hotspots.payment_method_picker_modal'); +final developer.UserTag _paymentMethodOtcSourceSheetProfileTag = + developer.UserTag('profile_hotspots.payment_method_otc_source_sheet'); +final developer.UserTag _flexAdjustFastPathProfileTag = + developer.UserTag('profile_hotspots.flex_adjust_fastpath'); +final developer.UserTag _flexNestedGroupFastPathProfileTag = + developer.UserTag('profile_hotspots.flex_nested_group_fastpath'); +final developer.UserTag _flexRunMetricsDenseProfileTag = + developer.UserTag('profile_hotspots.flex_runmetrics_dense'); +final developer.UserTag _flexTightFastPathDenseProfileTag = + developer.UserTag('profile_hotspots.flex_tight_fastpath_dense'); +final developer.UserTag _flexHybridFastPathDenseProfileTag = + developer.UserTag('profile_hotspots.flex_hybrid_fastpath_dense'); +final developer.UserTag _flexAdjustWidgetDenseProfileTag = + developer.UserTag('profile_hotspots.flex_adjust_widget_dense'); +const String _profileCaseFilter = String.fromEnvironment( + 'WEBF_PROFILE_CASE_FILTER', + defaultValue: '', +); void main() { final IntegrationTestWidgetsFlutterBinding binding = @@ -27,6 +57,10 @@ void main() { await _configureProfileTestEnvironment(); }); + tearDownAll(() async { + await _persistProfileArtifacts(binding.reportData); + }); + setUp(() { WebFControllerManager.instance.initialize( WebFControllerManagerConfig( @@ -42,8 +76,8 @@ void main() { await Future.delayed(const Duration(milliseconds: 100)); }); - group('profile hotspot cases', () { - testWidgets('profiles deep direction inheritance hotspot', + group('profile_hotspot_cases', () { + testWidgets('direction_inheritance', (WidgetTester tester) async { final _PreparedProfileCase prepared = await _prepareProfileCase( tester, @@ -75,9 +109,9 @@ void main() { ); expect(host.renderStyle.direction, TextDirection.rtl); - }); + }, skip: !_shouldRunProfileCase('direction_inheritance')); - testWidgets('profiles deep textAlign inheritance hotspot', + testWidgets('text_align_inheritance', (WidgetTester tester) async { final _PreparedProfileCase prepared = await _prepareProfileCase( tester, @@ -109,9 +143,9 @@ void main() { ); expect(host.renderStyle.textAlign, TextAlign.center); - }); + }, skip: !_shouldRunProfileCase('text_align_inheritance')); - testWidgets('profiles inline paragraph rebuild hotspot', + testWidgets('paragraph_rebuild', (WidgetTester tester) async { final _PreparedProfileCase prepared = await _prepareProfileCase( tester, @@ -160,9 +194,9 @@ void main() { expect(host.getBoundingClientRect().width, greaterThan(0)); expect(paragraph.getBoundingClientRect().height, greaterThan(0)); - }); + }, skip: !_shouldRunProfileCase('paragraph_rebuild')); - testWidgets('profiles opacity transition hotspot', + testWidgets('opacity_transition', (WidgetTester tester) async { final _PreparedProfileCase prepared = await _prepareProfileCase( tester, @@ -192,291 +226,6511 @@ void main() { ); expect(stage.className, isEmpty); - }); + }, skip: !_shouldRunProfileCase('opacity_transition')); - testWidgets('profiles flex inline layout hotspot', + testWidgets('fiat_filter_popup', (WidgetTester tester) async { final _PreparedProfileCase prepared = await _prepareProfileCase( tester, controllerName: - 'profile-flex-inline-${DateTime.now().millisecondsSinceEpoch}', - html: _buildFlexInlineLayoutHtml(cardCount: 72), + 'profile-fiat-popup-${DateTime.now().millisecondsSinceEpoch}', + html: _buildFiatFilterPopupHtml(optionCount: 64), ); final dom.Element host = prepared.getElementById('host'); - final dom.Element board = prepared.getElementById('board'); + final dom.Element popup = prepared.getElementById('popup'); binding.reportData ??= {}; - binding.reportData!['flex_inline_layout_meta'] = { - 'cardCount': 72, - 'mutationIterations': 32, - 'styleMutationPhases': 4, - 'layoutMode': 'no-flex-no-stretch-no-baseline-nowrap', + binding.reportData!['fiat_filter_popup_meta'] = { + 'optionCount': 64, + 'mutationIterations': 40, + 'layoutMode': 'fixed-height-popup-option-list', }; - await _runFlexInlineLayoutLoop( + await _runFiatFilterPopupLoop( prepared, mutationIterations: 10, - widths: const ['360px', '324px', '296px', '344px'], + widths: const ['364px', '328px', '388px', '340px'], ); - binding.reportData!['flex_inline_layout_cpu_samples'] = + binding.reportData!['fiat_filter_popup_cpu_samples'] = await _captureCpuSamples( - userTag: _flexInlineLayoutProfileTag, + userTag: _fiatFilterPopupProfileTag, action: () async { await binding.traceAction( () async { - await _runFlexInlineLayoutLoop( + await _runFiatFilterPopupLoop( prepared, - mutationIterations: 32, - widths: const ['360px', '324px', '296px', '344px'], + mutationIterations: 40, + widths: const ['364px', '328px', '388px', '340px'], ); }, - reportKey: 'flex_inline_layout_timeline', + reportKey: 'fiat_filter_popup_timeline', ); }, ); expect(host.getBoundingClientRect().width, greaterThan(0)); - expect(board.getBoundingClientRect().height, greaterThan(0)); - }); - }); -} + expect(popup.getBoundingClientRect().height, greaterThan(0)); + }, skip: !_shouldRunProfileCase('fiat_filter_popup')); -Future> _captureCpuSamples({ - required Future Function() action, - required developer.UserTag userTag, -}) async { - final developer.ServiceProtocolInfo info = await developer.Service.getInfo(); - final Uri? serviceUri = info.serverWebSocketUri; - if (serviceUri == null) { - throw StateError('VM service websocket URI is unavailable.'); - } + testWidgets('payment_method_sheet', + (WidgetTester tester) async { + final _PreparedProfileCase prepared = await _prepareProfileCase( + tester, + controllerName: + 'profile-payment-sheet-${DateTime.now().millisecondsSinceEpoch}', + html: _buildPaymentMethodSheetHtml(groupCount: 4, rowsPerGroup: 9), + ); - // ignore: deprecated_member_use - final String? isolateId = developer.Service.getIsolateID(Isolate.current); - if (isolateId == null) { - throw StateError('Current isolate is not visible to the VM service.'); - } + final dom.Element host = prepared.getElementById('host'); + final dom.Element sheet = prepared.getElementById('sheet'); - final vm.VmService service = await vmServiceConnectUri(serviceUri.toString()); - try { - final int startMicros = (await service.getVMTimelineMicros()).timestamp!; - final developer.UserTag previousTag = userTag.makeCurrent(); - try { - await action(); - } finally { - previousTag.makeCurrent(); - } + binding.reportData ??= {}; + binding.reportData!['payment_method_sheet_meta'] = { + 'groupCount': 4, + 'rowsPerGroup': 9, + 'mutationIterations': 36, + 'layoutMode': 'bottom-sheet-grouped-payment-list', + }; - final int endMicros = (await service.getVMTimelineMicros()).timestamp!; - final int timeExtentMicros = - endMicros > startMicros ? endMicros - startMicros : 1; - final vm.CpuSamples samples = - await service.getCpuSamples(isolateId, startMicros, timeExtentMicros); + await _runPaymentMethodSheetLoop( + prepared, + mutationIterations: 10, + widths: const ['378px', '342px', '312px', '356px'], + ); - return { - 'profileLabel': userTag.label, - 'isolateId': isolateId, - 'timeOriginMicros': startMicros, - 'timeExtentMicros': timeExtentMicros, - 'samples': samples.toJson(), - }; - } finally { - await service.dispose(); - } -} + binding.reportData!['payment_method_sheet_cpu_samples'] = + await _captureCpuSamples( + userTag: _paymentMethodSheetProfileTag, + action: () async { + await binding.traceAction( + () async { + await _runPaymentMethodSheetLoop( + prepared, + mutationIterations: 36, + widths: const ['378px', '342px', '312px', '356px'], + ); + }, + reportKey: 'payment_method_sheet_timeline', + ); + }, + ); -Future _configureProfileTestEnvironment() async { - NavigatorModule.setCustomUserAgent('webf/profile-tests'); + expect(host.getBoundingClientRect().width, greaterThan(0)); + expect(sheet.getBoundingClientRect().height, greaterThan(0)); + }, skip: !_shouldRunProfileCase('payment_method_sheet')); - final String? externalBridgePath = - Platform.environment['WEBF_PROFILE_EXTERNAL_BRIDGE_PATH']; - if (externalBridgePath != null && externalBridgePath.isNotEmpty) { - // The macOS test app already embeds libwebf.dylib. Forcing another path - // here loads a second copy of the bridge and splits bridge globals/TLS. - WebFDynamicLibrary.dynamicLibraryPath = path.normalize(externalBridgePath); - } + testWidgets('payment_method_bottom_sheet', + (WidgetTester tester) async { + final _PreparedProfileCase prepared = await _prepareProfileCase( + tester, + controllerName: + 'profile-payment-bottom-sheet-${DateTime.now().millisecondsSinceEpoch}', + html: _buildPaymentMethodBottomSheetHtml( + groupCount: 4, + rowsPerGroup: 9, + ), + ); - final Directory tempDirectory = Directory( - path.join(Directory.current.path, 'build', 'profile_test_temp'), - )..createSync(recursive: true); + await _pumpFrames(tester, 8); - final MethodChannel webfChannel = getWebFMethodChannel(); - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - webfChannel, - (MethodCall methodCall) async { - if (methodCall.method == 'getTemporaryDirectory') { - return tempDirectory.path; - } - throw FlutterError('Not implemented for method ${methodCall.method}.'); - }, - ); + final dom.Element host = prepared.getElementById('host'); + final dom.Element sheet = prepared.getElementById('sheet'); - const MethodChannel pathProviderChannel = - MethodChannel('plugins.flutter.io/path_provider'); - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - pathProviderChannel, - (MethodCall methodCall) async { - if (methodCall.method == 'getTemporaryDirectory') { - return tempDirectory.path; - } - throw FlutterError('Not implemented for method ${methodCall.method}.'); - }, - ); -} + binding.reportData ??= {}; + binding.reportData!['payment_method_bottom_sheet_meta'] = + { + 'groupCount': 4, + 'rowsPerGroup': 9, + 'mutationIterations': 36, + 'layoutMode': 'flutter-bottom-sheet-grouped-payment-list', + }; -Future<_PreparedProfileCase> _prepareProfileCase( - WidgetTester tester, { - required String controllerName, - required String html, - double viewportWidth = 390, - double viewportHeight = 844, -}) async { - tester.view.physicalSize = ui.Size(viewportWidth, viewportHeight); - tester.view.devicePixelRatio = 1.0; + await _runPaymentMethodSheetLoop( + prepared, + mutationIterations: 10, + widths: const ['378px', '342px', '312px', '356px'], + ); - WebFController? controller; - await tester.runAsync(() async { - controller = await WebFControllerManager.instance.addWithPreload( - name: controllerName, - createController: () => WebFController( - viewportWidth: viewportWidth, - viewportHeight: viewportHeight, - ), - bundle: WebFBundle.fromContent( - html, - url: 'test://$controllerName/', - contentType: htmlContentType, - ), - ); - await controller!.controlledInitCompleter.future; - }); + binding.reportData!['payment_method_bottom_sheet_cpu_samples'] = + await _captureCpuSamples( + userTag: _paymentMethodBottomSheetProfileTag, + action: () async { + await binding.traceAction( + () async { + await _runPaymentMethodSheetLoop( + prepared, + mutationIterations: 36, + widths: const ['378px', '342px', '312px', '356px'], + ); + }, + reportKey: 'payment_method_bottom_sheet_timeline', + ); + }, + ); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: WebF.fromControllerName(controllerName: controllerName), - ), - ), - ); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 120)); + expect(host.getBoundingClientRect().width, greaterThan(0)); + expect(sheet.getBoundingClientRect().height, greaterThan(0)); + }, skip: !_shouldRunProfileCase('payment_method_bottom_sheet')); - await tester.runAsync(() async { - await controller!.controllerPreloadingCompleter.future; - await Future.wait(>[ - controller!.controllerOnDOMContentLoadedCompleter.future, - controller!.viewportLayoutCompleter.future, - ]); - }); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 120)); + testWidgets('payment_method_fastpath_sheet', + (WidgetTester tester) async { + final _PreparedProfileCase prepared = await _prepareProfileCase( + tester, + controllerName: + 'profile-payment-fastpath-sheet-${DateTime.now().millisecondsSinceEpoch}', + html: _buildPaymentMethodFastPathSheetHtml( + groupCount: 4, + rowsPerGroup: 10, + ), + ); - return _PreparedProfileCase( - controller: controller!, - tester: tester, - ); -} + final dom.Element host = prepared.getElementById('host'); + final dom.Element sheet = prepared.getElementById('sheet'); -Future _toggleWidths( - _PreparedProfileCase prepared, - String elementId, { - required List widths, - required int iterations, -}) async { - for (int i = 0; i < iterations; i++) { - final String width = widths[i % widths.length]; - await prepared.evaluate( - 'document.getElementById(${jsonEncode(elementId)}).style.width = ' - '${jsonEncode(width)};', - ); - await _pumpFrames(prepared.tester, 2); - } -} + binding.reportData ??= {}; + binding.reportData!['payment_method_fastpath_sheet_meta'] = + { + 'groupCount': 4, + 'rowsPerGroup': 10, + 'mutationIterations': 36, + 'layoutMode': 'strict-fastpath-payment-sheet', + }; -Future _runOpacityCycle( - _PreparedProfileCase prepared, - String elementId, { - required int forwardFrames, - required int reverseFrames, -}) async { - await prepared.evaluate( - 'document.getElementById(${jsonEncode(elementId)}).className = "dim";', - ); - await _pumpFrames(prepared.tester, forwardFrames); + await _runPaymentMethodSheetLoop( + prepared, + mutationIterations: 10, + widths: const ['378px', '342px', '312px', '356px'], + ); - await prepared.evaluate( - 'document.getElementById(${jsonEncode(elementId)}).className = "";', - ); - await _pumpFrames(prepared.tester, reverseFrames); -} + binding.reportData!['payment_method_fastpath_sheet_cpu_samples'] = + await _captureCpuSamples( + userTag: _paymentMethodFastPathSheetProfileTag, + action: () async { + await binding.traceAction( + () async { + await _runPaymentMethodSheetLoop( + prepared, + mutationIterations: 36, + widths: const ['378px', '342px', '312px', '356px'], + ); + }, + reportKey: 'payment_method_fastpath_sheet_timeline', + ); + }, + ); -Future _runParagraphRebuildLoop( - _PreparedProfileCase prepared, { - required int mutationIterations, - required List widths, -}) async { - final dom.Element paragraph = prepared.getElementById('paragraph'); - for (int iteration = 0; iteration < mutationIterations; iteration++) { - final int phase = iteration % widths.length; + expect(host.getBoundingClientRect().width, greaterThan(0)); + expect(sheet.getBoundingClientRect().height, greaterThan(0)); + }, skip: !_shouldRunProfileCase('payment_method_fastpath_sheet')); - paragraph.setInlineStyle('width', widths[phase]); - paragraph.setInlineStyle('fontSize', phase.isEven ? '16px' : '17px'); - paragraph.setInlineStyle('lineHeight', phase >= 2 ? '24px' : '22px'); - paragraph.setInlineStyle('letterSpacing', phase == 1 ? '0.2px' : '0px'); - paragraph.style.flushPendingProperties(); - paragraph.className = 'phase-$phase'; + testWidgets('payment_method_bottom_sheet_tight', + (WidgetTester tester) async { + final _PreparedProfileCase prepared = await _prepareProfileCase( + tester, + controllerName: + 'profile-payment-bottom-sheet-tight-${DateTime.now().millisecondsSinceEpoch}', + html: _buildPaymentMethodBottomSheetTightHtml( + groupCount: 4, + rowsPerGroup: 9, + ), + ); - paragraph.ownerDocument.updateStyleIfNeeded(); - await _pumpFrames(prepared.tester, 2); - } + await prepared.evaluate( + ''' +(() => { + const rebuild = (id, tag) => { + const oldNode = document.getElementById(id); + if (!oldNode) return null; + const parent = oldNode.parentNode; + if (!parent) return oldNode; + const replacement = document.createElement(tag); + replacement.id = oldNode.id; + replacement.className = oldNode.className; + const attrs = oldNode.getAttributeNames ? oldNode.getAttributeNames() : []; + for (const name of attrs) { + if (name !== 'id' && name !== 'class') { + replacement.setAttribute(name, oldNode.getAttribute(name)); + } + } + while (oldNode.firstChild) { + replacement.appendChild(oldNode.firstChild); + } + parent.replaceChild(replacement, oldNode); + return replacement; + }; + + rebuild('sheet-popup-item', 'flutter-popup-item'); + const sheet = rebuild('payment-sheet', 'flutter-bottom-sheet'); + if (sheet && typeof sheet.open === 'function') { + sheet.open(); + } +})(); +''', + ); + await _pumpFrames(tester, 18); + + final dom.Element host = prepared.getElementById('host'); + final dom.Element sheet = prepared.getElementById('sheet'); + + binding.reportData ??= {}; + binding.reportData!['payment_method_bottom_sheet_tight_meta'] = + { + 'groupCount': 4, + 'rowsPerGroup': 9, + 'mutationIterations': 36, + 'layoutMode': 'flutter-bottom-sheet-tight-payment-list', + }; + + await _runPaymentMethodSheetLoop( + prepared, + mutationIterations: 10, + widths: const ['378px', '342px', '312px', '356px'], + ); + + binding.reportData!['payment_method_bottom_sheet_tight_cpu_samples'] = + await _captureCpuSamples( + userTag: _paymentMethodBottomSheetTightProfileTag, + action: () async { + await binding.traceAction( + () async { + await _runPaymentMethodSheetLoop( + prepared, + mutationIterations: 36, + widths: const ['378px', '342px', '312px', '356px'], + ); + }, + reportKey: 'payment_method_bottom_sheet_tight_timeline', + ); + }, + ); + + expect(host.getBoundingClientRect().width, greaterThan(0)); + expect(sheet.getBoundingClientRect().height, greaterThan(0)); + }, skip: !_shouldRunProfileCase('payment_method_bottom_sheet_tight')); + + testWidgets('payment_method_picker_modal', + (WidgetTester tester) async { + final _PreparedProfileCase prepared = await _prepareProfileCase( + tester, + controllerName: + 'profile-payment-picker-modal-${DateTime.now().millisecondsSinceEpoch}', + html: _buildPaymentMethodPickerModalHtml(), + ); + + await prepared.evaluate( + ''' +(() => { + const rebuild = (id, tag) => { + const oldNode = document.getElementById(id); + if (!oldNode) return null; + const parent = oldNode.parentNode; + if (!parent) return oldNode; + const replacement = document.createElement(tag); + replacement.id = oldNode.id; + replacement.className = oldNode.className; + const attrs = oldNode.getAttributeNames ? oldNode.getAttributeNames() : []; + for (const name of attrs) { + if (name !== 'id' && name !== 'class') { + replacement.setAttribute(name, oldNode.getAttribute(name)); + } + } + while (oldNode.firstChild) { + replacement.appendChild(oldNode.firstChild); + } + parent.replaceChild(replacement, oldNode); + return replacement; + }; + + const portal = rebuild('sheet-root', 'flutter-portal-popup-item'); + const modal = rebuild('payment-modal', 'flutter-modal-popup'); + if (modal && typeof modal.show === 'function') { + modal.show(); + } +})(); +''', + ); + await _pumpFrames(tester, 18); + + final dom.Element host = prepared.getElementById('host'); + final dom.Element sheet = prepared.getElementById('sheet-root'); + + binding.reportData ??= {}; + binding.reportData!['payment_method_picker_modal_meta'] = + { + 'sectionCount': 2, + 'cardCount': 4, + 'mutationIterations': 36, + 'layoutMode': 'widget-backed-bottom-sheet-payment-picker', + }; + + await _runPaymentMethodPickerModalLoop( + prepared, + mutationIterations: 10, + widths: const ['378px', '346px', '320px', '356px'], + ); + + binding.reportData!['payment_method_picker_modal_cpu_samples'] = + await _captureCpuSamples( + userTag: _paymentMethodPickerModalProfileTag, + action: () async { + await binding.traceAction( + () async { + await _runPaymentMethodPickerModalLoop( + prepared, + mutationIterations: 36, + widths: const ['378px', '346px', '320px', '356px'], + ); + }, + reportKey: 'payment_method_picker_modal_timeline', + ); + }, + ); + + expect(host.getBoundingClientRect().width, greaterThan(0)); + expect(sheet.getBoundingClientRect().height, greaterThan(0)); + }, skip: !_shouldRunProfileCase('payment_method_picker_modal')); + + testWidgets('payment_method_otc_source_sheet', + (WidgetTester tester) async { + final _PreparedProfileCase prepared = await _prepareProfileCase( + tester, + controllerName: + 'profile-payment-otc-source-${DateTime.now().millisecondsSinceEpoch}', + html: _buildPaymentMethodOtcSourceSheetHtml( + sectionCount: 1, + cardsPerSection: 5, + accountsPerCard: 3, + ), + ); + + await prepared.evaluate( + ''' +(() => { + const rebuild = (id, tag) => { + const oldNode = document.getElementById(id); + if (!oldNode) return null; + const parent = oldNode.parentNode; + if (!parent) return oldNode; + const replacement = document.createElement(tag); + replacement.id = oldNode.id; + replacement.className = oldNode.className; + const attrs = oldNode.getAttributeNames ? oldNode.getAttributeNames() : []; + for (const name of attrs) { + if (name !== 'id' && name !== 'class') { + replacement.setAttribute(name, oldNode.getAttribute(name)); + } + } + while (oldNode.firstChild) { + replacement.appendChild(oldNode.firstChild); + } + parent.replaceChild(replacement, oldNode); + return replacement; + }; + + rebuild('sheet-popup-item', 'flutter-popup-item'); + const sheet = rebuild('payment-sheet', 'flutter-bottom-sheet'); + if (sheet && typeof sheet.open === 'function') { + sheet.open(); + } +})(); +''', + ); + await _pumpFrames(tester, 18); + + final dom.Element host = prepared.getElementById('host'); + final dom.Element sheet = prepared.getElementById('sheet-root'); + + binding.reportData ??= {}; + binding.reportData!['payment_method_otc_source_sheet_meta'] = + { + 'sectionCount': 1, + 'cardsPerSection': 5, + 'accountsPerCard': 3, + 'mutationIterations': 4, + 'layoutMode': 'otc-payment-method-item-cardoption-bottom-sheet', + }; + + await _runPaymentMethodOtcSourceSheetLoop( + prepared, + mutationIterations: 1, + widths: const ['378px', '344px', '316px', '356px'], + ); + + binding.reportData!['payment_method_otc_source_sheet_cpu_samples'] = + await _captureCpuSamples( + userTag: _paymentMethodOtcSourceSheetProfileTag, + action: () async { + await binding.traceAction( + () async { + await _runPaymentMethodOtcSourceSheetLoop( + prepared, + mutationIterations: 4, + widths: const ['378px', '344px', '316px', '356px'], + ); + }, + reportKey: 'payment_method_otc_source_sheet_timeline', + ); + }, + ); + + expect(host.getBoundingClientRect().width, greaterThan(0)); + expect(sheet.getBoundingClientRect().height, greaterThan(0)); + }, skip: !_shouldRunProfileCase('payment_method_otc_source_sheet')); + + testWidgets('flex_inline_layout', + (WidgetTester tester) async { + final _PreparedProfileCase prepared = await _prepareProfileCase( + tester, + controllerName: + 'profile-flex-inline-${DateTime.now().millisecondsSinceEpoch}', + html: _buildFlexInlineLayoutHtml(cardCount: 48), + ); + + final dom.Element host = prepared.getElementById('host'); + final dom.Element board = prepared.getElementById('board'); + + binding.reportData ??= {}; + binding.reportData!['flex_inline_layout_meta'] = { + 'cardCount': 48, + 'mutationIterations': 32, + 'styleMutationPhases': 4, + 'layoutMode': 'mixed-fastpath-and-runmetrics-nowrap', + }; + + await _runFlexInlineLayoutLoop( + prepared, + mutationIterations: 10, + widths: const ['360px', '312px', '280px', '336px'], + ); + + binding.reportData!['flex_inline_layout_cpu_samples'] = + await _captureCpuSamples( + userTag: _flexInlineLayoutProfileTag, + action: () async { + await binding.traceAction( + () async { + await _runFlexInlineLayoutLoop( + prepared, + mutationIterations: 32, + widths: const ['360px', '312px', '280px', '336px'], + ); + }, + reportKey: 'flex_inline_layout_timeline', + ); + }, + ); + + expect(host.getBoundingClientRect().width, greaterThan(0)); + expect(board.getBoundingClientRect().height, greaterThan(0)); + }, skip: !_shouldRunProfileCase('flex_inline_layout')); + + testWidgets('flex_adjust_fastpath', + (WidgetTester tester) async { + final _PreparedProfileCase prepared = await _prepareProfileCase( + tester, + controllerName: + 'profile-flex-adjust-${DateTime.now().millisecondsSinceEpoch}', + html: _buildFlexAdjustFastPathHtml(cardCount: 48), + ); + + final dom.Element host = prepared.getElementById('host'); + final dom.Element board = prepared.getElementById('board'); + + binding.reportData ??= {}; + binding.reportData!['flex_adjust_fastpath_meta'] = { + 'cardCount': 48, + 'mutationIterations': 56, + 'styleMutationPhases': 4, + 'layoutMode': 'adjust-fastpath-heavy-inline-width-mutations', + }; + + await _runFlexRunMetricsDenseLoop( + prepared, + mutationIterations: 14, + widths: const ['432px', '388px', '352px', '408px'], + ); + + binding.reportData!['flex_adjust_fastpath_cpu_samples'] = + await _captureCpuSamples( + userTag: _flexAdjustFastPathProfileTag, + action: () async { + await binding.traceAction( + () async { + await _runFlexRunMetricsDenseLoop( + prepared, + mutationIterations: 56, + widths: const ['432px', '388px', '352px', '408px'], + ); + }, + reportKey: 'flex_adjust_fastpath_timeline', + ); + }, + ); + + expect(host.getBoundingClientRect().width, greaterThan(0)); + expect(board.getBoundingClientRect().height, greaterThan(0)); + }, skip: !_shouldRunProfileCase('flex_adjust_fastpath')); + + testWidgets('flex_nested_group_fastpath', + (WidgetTester tester) async { + final _PreparedProfileCase prepared = await _prepareProfileCase( + tester, + controllerName: + 'profile-flex-nested-group-${DateTime.now().millisecondsSinceEpoch}', + html: _buildFlexNestedGroupFastPathHtml(cardCount: 56), + ); + + final dom.Element host = prepared.getElementById('host'); + final dom.Element board = prepared.getElementById('board'); + + binding.reportData ??= {}; + binding.reportData!['flex_nested_group_fastpath_meta'] = + { + 'cardCount': 56, + 'mutationIterations': 56, + 'styleMutationPhases': 4, + 'layoutMode': 'nested-tight-group-fastpath-heavy-nowrap', + }; + + await _runFlexAdjustFastPathLoop( + prepared, + mutationIterations: 14, + widths: const ['432px', '388px', '352px', '408px'], + ); + + binding.reportData!['flex_nested_group_fastpath_cpu_samples'] = + await _captureCpuSamples( + userTag: _flexNestedGroupFastPathProfileTag, + action: () async { + await binding.traceAction( + () async { + await _runFlexAdjustFastPathLoop( + prepared, + mutationIterations: 56, + widths: const ['432px', '388px', '352px', '408px'], + ); + }, + reportKey: 'flex_nested_group_fastpath_timeline', + ); + }, + ); + + expect(host.getBoundingClientRect().width, greaterThan(0)); + expect(board.getBoundingClientRect().height, greaterThan(0)); + }, skip: !_shouldRunProfileCase('flex_nested_group_fastpath')); + + testWidgets('flex_runmetrics_dense', + (WidgetTester tester) async { + final _PreparedProfileCase prepared = await _prepareProfileCase( + tester, + controllerName: + 'profile-flex-runmetrics-${DateTime.now().millisecondsSinceEpoch}', + html: _buildFlexRunMetricsDenseHtml(cardCount: 60), + ); + + final dom.Element host = prepared.getElementById('host'); + final dom.Element board = prepared.getElementById('board'); + + binding.reportData ??= {}; + binding.reportData!['flex_runmetrics_dense_meta'] = { + 'cardCount': 60, + 'mutationIterations': 56, + 'styleMutationPhases': 4, + 'layoutMode': 'dense-runmetrics-rows-nowrap', + }; + + await _runFlexAdjustFastPathLoop( + prepared, + mutationIterations: 14, + widths: const ['372px', '322px', '286px', '344px'], + ); + + binding.reportData!['flex_runmetrics_dense_cpu_samples'] = + await _captureCpuSamples( + userTag: _flexRunMetricsDenseProfileTag, + action: () async { + await binding.traceAction( + () async { + await _runFlexAdjustFastPathLoop( + prepared, + mutationIterations: 56, + widths: const ['372px', '322px', '286px', '344px'], + ); + }, + reportKey: 'flex_runmetrics_dense_timeline', + ); + }, + ); + + expect(host.getBoundingClientRect().width, greaterThan(0)); + expect(board.getBoundingClientRect().height, greaterThan(0)); + }, skip: !_shouldRunProfileCase('flex_runmetrics_dense')); + + testWidgets('flex_tight_fastpath_dense', + (WidgetTester tester) async { + final _PreparedProfileCase prepared = await _prepareProfileCase( + tester, + controllerName: + 'profile-flex-tight-fastpath-${DateTime.now().millisecondsSinceEpoch}', + html: _buildFlexTightFastPathDenseHtml(cardCount: 60), + ); + + final dom.Element host = prepared.getElementById('host'); + final dom.Element board = prepared.getElementById('board'); + + binding.reportData ??= {}; + binding.reportData!['flex_tight_fastpath_dense_meta'] = + { + 'cardCount': 60, + 'mutationIterations': 56, + 'styleMutationPhases': 4, + 'layoutMode': 'tight-fastpath-dense-rows-nowrap', + }; + + await _runFlexTightFastPathDenseLoop( + prepared, + mutationIterations: 14, + widths: const ['372px', '322px', '286px', '344px'], + ); + + binding.reportData!['flex_tight_fastpath_dense_cpu_samples'] = + await _captureCpuSamples( + userTag: _flexTightFastPathDenseProfileTag, + action: () async { + await binding.traceAction( + () async { + await _runFlexTightFastPathDenseLoop( + prepared, + mutationIterations: 56, + widths: const ['372px', '322px', '286px', '344px'], + ); + }, + reportKey: 'flex_tight_fastpath_dense_timeline', + ); + }, + ); + + expect(host.getBoundingClientRect().width, greaterThan(0)); + expect(board.getBoundingClientRect().height, greaterThan(0)); + }, skip: !_shouldRunProfileCase('flex_tight_fastpath_dense')); + + testWidgets('flex_hybrid_fastpath_dense', + (WidgetTester tester) async { + final _PreparedProfileCase prepared = await _prepareProfileCase( + tester, + controllerName: + 'profile-flex-hybrid-fastpath-${DateTime.now().millisecondsSinceEpoch}', + html: _buildFlexHybridFastPathDenseHtml(cardCount: 60), + ); + + final dom.Element host = prepared.getElementById('host'); + final dom.Element board = prepared.getElementById('board'); + + binding.reportData ??= {}; + binding.reportData!['flex_hybrid_fastpath_dense_meta'] = + { + 'cardCount': 60, + 'mutationIterations': 56, + 'styleMutationPhases': 4, + 'layoutMode': 'hybrid-dense-runmetrics-tight-widget-rows-nowrap', + }; + + await _runFlexHybridFastPathDenseLoop( + prepared, + mutationIterations: 14, + widths: const ['372px', '322px', '286px', '344px'], + ); + + binding.reportData!['flex_hybrid_fastpath_dense_cpu_samples'] = + await _captureCpuSamples( + userTag: _flexHybridFastPathDenseProfileTag, + action: () async { + await binding.traceAction( + () async { + await _runFlexHybridFastPathDenseLoop( + prepared, + mutationIterations: 56, + widths: const ['372px', '322px', '286px', '344px'], + ); + }, + reportKey: 'flex_hybrid_fastpath_dense_timeline', + ); + }, + ); + + expect(host.getBoundingClientRect().width, greaterThan(0)); + expect(board.getBoundingClientRect().height, greaterThan(0)); + }, skip: !_shouldRunProfileCase('flex_hybrid_fastpath_dense')); + + testWidgets('flex_adjust_widget_dense', + (WidgetTester tester) async { + final _PreparedProfileCase prepared = await _prepareProfileCase( + tester, + controllerName: + 'profile-flex-adjust-widget-${DateTime.now().millisecondsSinceEpoch}', + html: _buildFlexAdjustWidgetDenseHtml(cardCount: 56), + ); + + final dom.Element host = prepared.getElementById('host'); + final dom.Element board = prepared.getElementById('board'); + + binding.reportData ??= {}; + binding.reportData!['flex_adjust_widget_dense_meta'] = { + 'cardCount': 56, + 'mutationIterations': 56, + 'styleMutationPhases': 4, + 'layoutMode': 'widget-dense-runmetrics-and-adjust-nowrap', + }; + + await _runFlexAdjustWidgetDenseLoop( + prepared, + mutationIterations: 14, + widths: const ['384px', '336px', '304px', '356px'], + ); + + binding.reportData!['flex_adjust_widget_dense_cpu_samples'] = + await _captureCpuSamples( + userTag: _flexAdjustWidgetDenseProfileTag, + action: () async { + await binding.traceAction( + () async { + await _runFlexAdjustWidgetDenseLoop( + prepared, + mutationIterations: 56, + widths: const ['384px', '336px', '304px', '356px'], + ); + }, + reportKey: 'flex_adjust_widget_dense_timeline', + ); + }, + ); + + expect(host.getBoundingClientRect().width, greaterThan(0)); + expect(board.getBoundingClientRect().height, greaterThan(0)); + }, skip: !_shouldRunProfileCase('flex_adjust_widget_dense')); + }); +} + +bool _shouldRunProfileCase(String caseId) { + final String trimmedFilter = _profileCaseFilter.trim(); + if (trimmedFilter.isEmpty) { + return true; + } + + final Set enabledCaseIds = trimmedFilter + .split(',') + .map((String value) => value.trim()) + .where((String value) => value.isNotEmpty) + .toSet(); + return enabledCaseIds.contains(caseId); +} + +Future> _captureCpuSamples({ + required Future Function() action, + required developer.UserTag userTag, +}) async { + if (_shouldCaptureCpuSamplesOnDriverSide()) { + final developer.UserTag previousTag = userTag.makeCurrent(); + try { + await action(); + } finally { + previousTag.makeCurrent(); + } + + return { + 'captureMode': 'driver', + 'profileLabel': userTag.label, + }; + } + + final developer.ServiceProtocolInfo info = await developer.Service.getInfo(); + final Uri? serviceUri = info.serverUri; + if (serviceUri == null) { + throw StateError('VM service URI is unavailable.'); + } + + // ignore: deprecated_member_use + final String? isolateId = developer.Service.getIsolateID(Isolate.current); + if (isolateId == null) { + throw StateError('Current isolate is not visible to the VM service.'); + } + + final String vmServiceAddress = + 'ws://localhost:${serviceUri.port}${serviceUri.path}ws'; + final vm.VmService service = await vmServiceConnectUri(vmServiceAddress); + try { + final int startMicros = (await service.getVMTimelineMicros()).timestamp!; + final developer.UserTag previousTag = userTag.makeCurrent(); + try { + await action(); + } finally { + previousTag.makeCurrent(); + } + + final int endMicros = (await service.getVMTimelineMicros()).timestamp!; + final int timeExtentMicros = + endMicros > startMicros ? endMicros - startMicros : 1; + final vm.CpuSamples samples = + await service.getCpuSamples(isolateId, startMicros, timeExtentMicros); + + return { + 'profileLabel': userTag.label, + 'isolateId': isolateId, + 'timeOriginMicros': startMicros, + 'timeExtentMicros': timeExtentMicros, + 'samples': samples.toJson(), + }; + } finally { + await service.dispose(); + } +} + +bool _shouldCaptureCpuSamplesOnDriverSide() { + return Platform.isAndroid || Platform.isIOS; +} + +Future _persistProfileArtifacts(Map? data) async { + if (data == null || data.isEmpty) { + return; + } + + final Directory outputDirectory = Directory(_profileArtifactsDirectoryPath()) + ..createSync(recursive: true); + + final Map response = Map.from(data); + final Map manifest = {}; + + await _writeProfileJson( + outputDirectory, + response, + outputFilename: 'all_cases', + ); + + for (final MapEntry entry in response.entries) { + if ((entry.key.endsWith('_timeline') || + entry.key.endsWith('_cpu_samples')) && + entry.value is Map) { + await _writeProfileJson( + outputDirectory, + Map.from(entry.value as Map), + outputFilename: entry.key, + ); + manifest[entry.key] = { + 'path': path.join(outputDirectory.path, '${entry.key}.json'), + }; + } else { + manifest[entry.key] = entry.value; + } + } + + await _writeProfileJson( + outputDirectory, + manifest, + outputFilename: 'manifest', + ); +} + +Future _writeProfileJson( + Directory outputDirectory, + Object data, { + required String outputFilename, +}) async { + final File file = File( + path.join(outputDirectory.path, '$outputFilename.json'), + ); + await file.writeAsString( + const JsonEncoder.withIndent(' ').convert(data), + ); +} + +Future _configureProfileTestEnvironment() async { + NavigatorModule.setCustomUserAgent('webf/profile-tests'); + + final String? externalBridgePath = + Platform.environment['WEBF_PROFILE_EXTERNAL_BRIDGE_PATH']; + if (externalBridgePath != null && externalBridgePath.isNotEmpty) { + // The macOS test app already embeds libwebf.dylib. Forcing another path + // here loads a second copy of the bridge and splits bridge globals/TLS. + WebFDynamicLibrary.dynamicLibraryPath = path.normalize(externalBridgePath); + } + + final Directory tempDirectory = Directory(_profileTempDirectoryPath()) + ..createSync(recursive: true); + + final MethodChannel webfChannel = getWebFMethodChannel(); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + webfChannel, + (MethodCall methodCall) async { + if (methodCall.method == 'getTemporaryDirectory') { + return tempDirectory.path; + } + throw FlutterError('Not implemented for method ${methodCall.method}.'); + }, + ); + + const MethodChannel pathProviderChannel = + MethodChannel('plugins.flutter.io/path_provider'); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + pathProviderChannel, + (MethodCall methodCall) async { + if (methodCall.method == 'getTemporaryDirectory') { + return tempDirectory.path; + } + throw FlutterError('Not implemented for method ${methodCall.method}.'); + }, + ); +} + +String _profileArtifactsDirectoryPath() { + return path.join(_profileFilesystemBasePath(), 'build', 'profile_hotspots'); +} + +String _profileTempDirectoryPath() { + return path.join(_profileFilesystemBasePath(), 'build', 'profile_test_temp'); +} + +String _profileFilesystemBasePath() { + if (Platform.isAndroid || Platform.isIOS) { + return path.join(Directory.systemTemp.path, 'webf_profile_tests'); + } + return Directory.current.path; +} + +Future<_PreparedProfileCase> _prepareProfileCase( + WidgetTester tester, { + required String controllerName, + required String html, + double viewportWidth = 390, + double viewportHeight = 844, +}) async { + tester.view.physicalSize = ui.Size(viewportWidth, viewportHeight); + tester.view.devicePixelRatio = 1.0; + + WebFController? controller; + await tester.runAsync(() async { + controller = await WebFControllerManager.instance.addWithPreload( + name: controllerName, + createController: () => WebFController( + viewportWidth: viewportWidth, + viewportHeight: viewportHeight, + ), + bundle: WebFBundle.fromContent( + html, + url: 'test://$controllerName/', + contentType: htmlContentType, + ), + ); + await controller!.controlledInitCompleter.future; + }); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: WebF.fromControllerName(controllerName: controllerName), + ), + ), + ); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 120)); + + await tester.runAsync(() async { + await controller!.controllerPreloadingCompleter.future; + await Future.wait(>[ + controller!.controllerOnDOMContentLoadedCompleter.future, + controller!.viewportLayoutCompleter.future, + ]); + }); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 120)); + + return _PreparedProfileCase( + controller: controller!, + tester: tester, + ); +} + +Future _toggleWidths( + _PreparedProfileCase prepared, + String elementId, { + required List widths, + required int iterations, +}) async { + for (int i = 0; i < iterations; i++) { + final String width = widths[i % widths.length]; + await prepared.evaluate( + 'document.getElementById(${jsonEncode(elementId)}).style.width = ' + '${jsonEncode(width)};', + ); + await _pumpFrames(prepared.tester, 2); + } +} + +Future _runOpacityCycle( + _PreparedProfileCase prepared, + String elementId, { + required int forwardFrames, + required int reverseFrames, +}) async { + await prepared.evaluate( + 'document.getElementById(${jsonEncode(elementId)}).className = "dim";', + ); + await _pumpFrames(prepared.tester, forwardFrames); + + await prepared.evaluate( + 'document.getElementById(${jsonEncode(elementId)}).className = "";', + ); + await _pumpFrames(prepared.tester, reverseFrames); +} + +Future _runParagraphRebuildLoop( + _PreparedProfileCase prepared, { + required int mutationIterations, + required List widths, +}) async { + final dom.Element paragraph = prepared.getElementById('paragraph'); + for (int iteration = 0; iteration < mutationIterations; iteration++) { + final int phase = iteration % widths.length; + + paragraph.setInlineStyle('width', widths[phase]); + paragraph.setInlineStyle('fontSize', phase.isEven ? '16px' : '17px'); + paragraph.setInlineStyle('lineHeight', phase >= 2 ? '24px' : '22px'); + paragraph.setInlineStyle('letterSpacing', phase == 1 ? '0.2px' : '0px'); + paragraph.style.flushPendingProperties(); + paragraph.className = 'phase-$phase'; + + paragraph.ownerDocument.updateStyleIfNeeded(); + await _pumpFrames(prepared.tester, 2); + } +} + +Future _runFiatFilterPopupLoop( + _PreparedProfileCase prepared, { + required int mutationIterations, + required List widths, +}) async { + final dom.Element host = prepared.getElementById('host'); + final dom.Element popup = prepared.getElementById('popup'); + final dom.Element trigger = prepared.getElementById('trigger'); + final dom.Element triggerValue = prepared.getElementById('trigger-value'); + final List options = popup.querySelectorAll(['.fiat-option']); + final List icons = popup.querySelectorAll(['.fiat-icon']); + final List copies = popup.querySelectorAll(['.fiat-copy']); + final List codes = popup.querySelectorAll(['.fiat-code']); + final List names = popup.querySelectorAll(['.fiat-name']); + final List badges = popup.querySelectorAll(['.fiat-badge']); + for (int iteration = 0; iteration < mutationIterations; iteration++) { + final int phase = iteration % widths.length; + host.setInlineStyle('width', widths[phase]); + host.setInlineStyle('padding', phase.isEven ? '10px' : '8px'); + popup.setInlineStyle( + 'padding', + phase == 1 ? '8px 10px' : (phase == 2 ? '10px 12px' : '9px 11px'), + ); + popup.setInlineStyle('gap', phase == 2 ? '6px' : '8px'); + trigger.setInlineStyle( + 'padding', + phase == 3 ? '10px 11px' : (phase == 1 ? '8px 10px' : '9px 11px'), + ); + triggerValue.setInlineStyle( + 'letterSpacing', + phase == 2 ? '0.18px' : (phase == 1 ? '0.08px' : '0px'), + ); + + final int optionGap = phase == 2 ? 10 : 8; + final int iconSize = phase == 1 ? 26 : (phase == 2 ? 30 : 28); + final int copyWidth = + phase == 0 ? 208 : (phase == 1 ? 192 : (phase == 2 ? 220 : 198)); + final int badgeWidth = phase == 2 ? 44 : (phase == 1 ? 36 : 40); + final String codeSpacing = + phase == 2 ? '0.24px' : (phase == 1 ? '0.10px' : '0px'); + final String nameSpacing = phase == 3 ? '0.18px' : '0px'; + + for (int index = 0; index < options.length; index++) { + final dom.Element element = options[index] as dom.Element; + final bool selected = index % 8 == phase; + element.className = selected ? 'fiat-option selected' : 'fiat-option'; + element.setInlineStyle('gap', '${optionGap}px'); + element.setInlineStyle( + 'padding', + phase == 1 ? '8px 6px' : (phase == 2 ? '10px 8px' : '9px 7px'), + ); + } + for (final dynamic node in icons) { + final dom.Element element = node as dom.Element; + element.setInlineStyle('width', '${iconSize}px'); + element.setInlineStyle('height', '${iconSize}px'); + } + for (int index = 0; index < copies.length; index++) { + final dom.Element element = copies[index] as dom.Element; + final int variance = index.isEven ? 0 : (index % 3 == 0 ? 10 : -8); + element.setInlineStyle('flexBasis', '${copyWidth + variance}px'); + } + for (final dynamic node in codes) { + final dom.Element element = node as dom.Element; + element.setInlineStyle('letterSpacing', codeSpacing); + element.setInlineStyle('paddingBottom', phase == 1 ? '2px' : '3px'); + } + for (final dynamic node in names) { + final dom.Element element = node as dom.Element; + element.setInlineStyle('letterSpacing', nameSpacing); + element.setInlineStyle('wordSpacing', phase == 2 ? '0.28px' : '0px'); + } + for (final dynamic node in badges) { + (node as dom.Element).setInlineStyle('width', '${badgeWidth}px'); + } + + popup.style.flushPendingProperties(); + popup.ownerDocument.updateStyleIfNeeded(); + await _pumpFrames(prepared.tester, 3); + } +} + +Future _runPaymentMethodSheetLoop( + _PreparedProfileCase prepared, { + required int mutationIterations, + required List widths, +}) async { + final dom.Element host = prepared.getElementById('host'); + final dom.Element sheet = prepared.getElementById('sheet'); + final dom.Element? popupItem = prepared.controller.view.document.getElementById( + ['sheet-popup-item'], + ); + final List groups = sheet.querySelectorAll(['.group']); + final List rows = sheet.querySelectorAll(['.payment-row']); + final List icons = sheet.querySelectorAll(['.payment-icon']); + final List statuses = sheet.querySelectorAll(['.payment-status']); + final List copies = sheet.querySelectorAll(['.payment-copy']); + final List titles = sheet.querySelectorAll(['.payment-title']); + final List subtitles = sheet.querySelectorAll(['.payment-subtitle']); + final List badgeRows = sheet.querySelectorAll(['.payment-badges']); + final List chips = sheet.querySelectorAll(['.payment-chip']); + final List rates = sheet.querySelectorAll(['.payment-rate']); + final List routes = sheet.querySelectorAll(['.payment-route']); + final List tails = sheet.querySelectorAll(['.payment-tail']); + final List showMoreRows = sheet.querySelectorAll(['.show-more-row']); + for (int iteration = 0; iteration < mutationIterations; iteration++) { + final int phase = iteration % widths.length; + host.setInlineStyle('width', widths[phase]); + host.setInlineStyle('padding', phase.isEven ? '10px 0 0' : '8px 0 0'); + if (popupItem != null) { + popupItem.setInlineStyle( + 'width', + phase == 0 ? '362px' : (phase == 1 ? '336px' : (phase == 2 ? '308px' : '348px')), + ); + popupItem.setInlineStyle( + 'padding', + phase == 2 ? '0 0 3px' : (phase == 1 ? '0 0 1px' : '0'), + ); + popupItem.setInlineStyle('boxSizing', 'border-box'); + } + sheet.setInlineStyle( + 'padding', + phase == 1 ? '15px 14px 18px' : (phase == 2 ? '17px 16px 20px' : '16px 15px 18px'), + ); + sheet.setInlineStyle('gap', phase == 2 ? '20px' : '24px'); + + final int rowGap = phase == 2 ? 10 : 8; + final int rowPadding = phase == 1 ? 9 : 10; + final int iconWidth = phase == 2 ? 38 : 34; + final int statusWidth = phase == 3 ? 42 : 38; + final int copyWidth = + phase == 0 ? 188 : (phase == 1 ? 172 : (phase == 2 ? 204 : 180)); + final int rateWidth = phase == 2 ? 52 : 46; + final int routeWidth = phase == 1 ? 78 : (phase == 2 ? 88 : 82); + final int tailWidth = phase == 3 ? 26 : 22; + final int chipWidth = phase == 2 ? 42 : (phase == 1 ? 34 : 38); + final String titleSpacing = + phase == 2 ? '0.18px' : (phase == 1 ? '0.08px' : '0px'); + final String subtitleSpacing = phase == 3 ? '0.14px' : '0px'; + + for (int index = 0; index < groups.length; index++) { + final dom.Element element = groups[index] as dom.Element; + final bool expanded = (iteration + index) % 3 != 0; + element.className = expanded ? 'group expanded' : 'group collapsed'; + element.setInlineStyle('gap', phase == 2 ? '10px' : '8px'); + } + for (int index = 0; index < rows.length; index++) { + final dom.Element element = rows[index] as dom.Element; + final bool selected = (index + phase) % 7 == 0; + final bool extra = element.getAttribute('data-extra') == 'true'; + final String selectionClass = selected ? ' selected' : ''; + element.className = extra + ? 'payment-row extra$selectionClass' + : 'payment-row$selectionClass'; + element.setInlineStyle('gap', '${rowGap}px'); + element.setInlineStyle('padding', '${rowPadding}px 0'); + } + for (final dynamic node in icons) { + (node as dom.Element).setInlineStyle('width', '${iconWidth}px'); + } + for (final dynamic node in statuses) { + (node as dom.Element).setInlineStyle('width', '${statusWidth}px'); + } + for (int index = 0; index < copies.length; index++) { + final dom.Element element = copies[index] as dom.Element; + final int variance = index.isEven ? 0 : (index % 3 == 0 ? 10 : -8); + element.setInlineStyle('width', '${copyWidth + variance}px'); + } + for (final dynamic node in titles) { + (node as dom.Element).setInlineStyle('letterSpacing', titleSpacing); + } + for (final dynamic node in subtitles) { + final dom.Element element = node as dom.Element; + element.setInlineStyle('letterSpacing', subtitleSpacing); + element.setInlineStyle('wordSpacing', phase == 2 ? '0.24px' : '0px'); + } + for (final dynamic node in badgeRows) { + (node as dom.Element).setInlineStyle('gap', phase == 2 ? '5px' : '4px'); + } + for (final dynamic node in chips) { + (node as dom.Element).setInlineStyle('width', '${chipWidth}px'); + } + for (final dynamic node in rates) { + (node as dom.Element).setInlineStyle('width', '${rateWidth}px'); + } + for (final dynamic node in routes) { + (node as dom.Element).setInlineStyle('width', '${routeWidth}px'); + } + for (final dynamic node in tails) { + (node as dom.Element).setInlineStyle('width', '${tailWidth}px'); + } + for (final dynamic node in showMoreRows) { + (node as dom.Element).setInlineStyle( + 'paddingTop', + phase == 1 ? '2px' : '0px', + ); + } + + sheet.style.flushPendingProperties(); + sheet.ownerDocument.updateStyleIfNeeded(); + await _pumpFrames(prepared.tester, 3); + } +} + +Future _runPaymentMethodPickerModalLoop( + _PreparedProfileCase prepared, { + required int mutationIterations, + required List widths, +}) async { + final dom.Element host = prepared.getElementById('host'); + final dom.Element summary = prepared.getElementById('summary-card'); + final dom.Element sheet = prepared.getElementById('sheet-root'); + final dom.Element secondarySection = prepared.getElementById('secondary-group'); + final List cards = sheet.querySelectorAll(['.method-card']); + final List headers = sheet.querySelectorAll(['.method-header']); + final List avatars = sheet.querySelectorAll(['.method-avatar']); + final List copies = sheet.querySelectorAll(['.method-copy']); + final List titles = sheet.querySelectorAll(['.method-title']); + final List descriptions = sheet.querySelectorAll(['.method-desc']); + final List prices = sheet.querySelectorAll(['.method-price']); + final List tags = sheet.querySelectorAll(['.method-tag']); + final List accountRows = sheet.querySelectorAll(['.account-row']); + final List accountNames = sheet.querySelectorAll(['.account-name']); + final List accountRadios = sheet.querySelectorAll(['.account-radio']); + final List accountBadges = sheet.querySelectorAll(['.account-badge']); + final List addRows = sheet.querySelectorAll(['.add-account']); + final List showMoreRows = sheet.querySelectorAll(['.show-more-row']); + for (int iteration = 0; iteration < mutationIterations; iteration++) { + final int phase = iteration % widths.length; + host.setInlineStyle('width', widths[phase]); + host.setInlineStyle('padding', phase.isEven ? '10px' : '8px'); + summary.setInlineStyle( + 'padding', + phase == 1 ? '18px 16px' : (phase == 2 ? '20px 18px' : '19px 17px'), + ); + sheet.setInlineStyle( + 'width', + phase == 2 ? '336px' : (phase == 1 ? '352px' : '364px'), + ); + sheet.setInlineStyle( + 'padding', + phase == 2 ? '4px 0 6px' : (phase == 1 ? '2px 0 4px' : '3px 0 5px'), + ); + + final int avatarWidth = phase == 2 ? 36 : 32; + final int copyWidth = + phase == 0 ? 188 : (phase == 1 ? 172 : (phase == 2 ? 204 : 180)); + final int priceWidth = phase == 2 ? 70 : (phase == 1 ? 58 : 64); + final int tagWidth = phase == 3 ? 78 : (phase == 1 ? 68 : 72); + final int radioWidth = phase == 2 ? 16 : 14; + final int badgeWidth = phase == 3 ? 46 : 40; + final String titleSpacing = + phase == 2 ? '0.14px' : (phase == 1 ? '0.08px' : '0px'); + final String descSpacing = phase == 3 ? '0.12px' : '0px'; + final String accountSpacing = + phase == 2 ? '0.10px' : (phase == 1 ? '0.06px' : '0px'); + + secondarySection.className = (iteration % 3 == 0) + ? 'section secondary-group extra-hidden' + : 'section secondary-group extra-visible'; + + for (int index = 0; index < cards.length; index++) { + final dom.Element element = cards[index] as dom.Element; + final String method = element.getAttribute('data-method') ?? ''; + final bool selected = index == phase; + final bool expandable = method != 'cvpay'; + final bool expanded = expandable && ((iteration + index) % 2 == 0); + final String selectedClass = selected ? ' selected' : ''; + final String expandedClass = expanded ? ' expanded' : ' collapsed'; + element.className = 'method-card$selectedClass$expandedClass'; + } + for (final dynamic node in headers) { + (node as dom.Element).setInlineStyle('padding', phase == 1 ? '14px 14px' : '15px 16px'); + } + for (final dynamic node in avatars) { + final dom.Element element = node as dom.Element; + element.setInlineStyle('width', '${avatarWidth}px'); + element.setInlineStyle('height', '${avatarWidth}px'); + } + for (int index = 0; index < copies.length; index++) { + final dom.Element element = copies[index] as dom.Element; + final int variance = index.isEven ? 0 : (index % 3 == 0 ? 10 : -8); + element.setInlineStyle('width', '${copyWidth + variance}px'); + } + for (final dynamic node in titles) { + (node as dom.Element).setInlineStyle('letterSpacing', titleSpacing); + } + for (final dynamic node in descriptions) { + final dom.Element element = node as dom.Element; + element.setInlineStyle('letterSpacing', descSpacing); + element.setInlineStyle('wordSpacing', phase == 2 ? '0.20px' : '0px'); + } + for (final dynamic node in prices) { + (node as dom.Element).setInlineStyle('width', '${priceWidth}px'); + } + for (final dynamic node in tags) { + (node as dom.Element).setInlineStyle('width', '${tagWidth}px'); + } + for (final dynamic node in accountRows) { + (node as dom.Element).setInlineStyle('gap', phase == 2 ? '10px' : '8px'); + } + for (final dynamic node in accountNames) { + final dom.Element element = node as dom.Element; + element.setInlineStyle('letterSpacing', accountSpacing); + element.setInlineStyle('wordSpacing', phase == 2 ? '0.18px' : '0px'); + } + for (final dynamic node in accountRadios) { + final dom.Element element = node as dom.Element; + element.setInlineStyle('width', '${radioWidth}px'); + element.setInlineStyle('height', '${radioWidth}px'); + } + for (final dynamic node in accountBadges) { + (node as dom.Element).setInlineStyle('width', '${badgeWidth}px'); + } + for (final dynamic node in addRows) { + (node as dom.Element).setInlineStyle('paddingTop', phase == 1 ? '10px' : '8px'); + } + for (final dynamic node in showMoreRows) { + (node as dom.Element).setInlineStyle('paddingTop', phase == 1 ? '4px' : '2px'); + } + + sheet.style.flushPendingProperties(); + sheet.ownerDocument.updateStyleIfNeeded(); + await _pumpFrames(prepared.tester, 3); + } +} + +Future _runPaymentMethodOtcSourceSheetLoop( + _PreparedProfileCase prepared, { + required int mutationIterations, + required List widths, +}) async { + final dom.Element host = prepared.getElementById('host'); + final dom.Element sheet = prepared.getElementById('sheet-root'); + final dom.Element? popupItem = prepared.controller.view.document.getElementById( + ['sheet-popup-item'], + ); + final List sections = sheet.querySelectorAll(['.otc-section']); + final List cards = sheet.querySelectorAll(['.otc-card']); + final List headers = sheet.querySelectorAll(['.otc-card-header']); + final List leadings = sheet.querySelectorAll(['.otc-card-leading']); + final List copies = sheet.querySelectorAll(['.otc-card-copy']); + final List nameRows = sheet.querySelectorAll(['.otc-card-name-row']); + final List names = sheet.querySelectorAll(['.otc-card-name']); + final List descriptions = + sheet.querySelectorAll(['.otc-card-description']); + final List prices = sheet.querySelectorAll(['.otc-card-price']); + final List tags = sheet.querySelectorAll(['.otc-card-tag']); + final List accountRows = sheet.querySelectorAll(['.otc-account-row']); + final List accountBoxes = + sheet.querySelectorAll(['.otc-account-box']); + final List accountItems = + sheet.querySelectorAll(['.otc-account-item']); + final List accountMains = + sheet.querySelectorAll(['.otc-account-main']); + final List accountCopies = + sheet.querySelectorAll(['.otc-account-copy']); + final List accountNames = + sheet.querySelectorAll(['.otc-account-name']); + final List accountDescriptions = + sheet.querySelectorAll(['.otc-account-description']); + final List accountStatuses = + sheet.querySelectorAll(['.otc-account-status']); + final List accountDeletes = + sheet.querySelectorAll(['.otc-account-delete']); + final List addAccounts = + sheet.querySelectorAll(['.otc-add-account']); + final List showMoreRows = + sheet.querySelectorAll(['.otc-show-more-row']); + + for (int iteration = 0; iteration < mutationIterations; iteration++) { + final int phase = iteration % widths.length; + host.setInlineStyle('width', widths[phase]); + host.setInlineStyle('padding', phase.isEven ? '10px 0 0' : '8px 0 0'); + if (popupItem != null) { + popupItem.setInlineStyle( + 'width', + phase == 0 ? '366px' : (phase == 1 ? '338px' : (phase == 2 ? '306px' : '352px')), + ); + popupItem.setInlineStyle( + 'padding', + phase == 2 ? '0 0 3px' : (phase == 1 ? '0 0 1px' : '0'), + ); + popupItem.setInlineStyle('boxSizing', 'border-box'); + } + + sheet.setInlineStyle( + 'padding', + phase == 2 ? '16px 14px 22px' : (phase == 1 ? '15px 13px 20px' : '16px 15px 21px'), + ); + sheet.setInlineStyle('gap', phase == 2 ? '22px' : '24px'); + + final int headerPaddingInline = phase == 2 ? 11 : 12; + final int headerPaddingBlock = phase == 1 ? 15 : 16; + final int leadingGap = phase == 2 ? 10 : 8; + final int copyWidth = + phase == 0 ? 190 : (phase == 1 ? 176 : (phase == 2 ? 206 : 184)); + final int priceWidth = phase == 2 ? 74 : (phase == 1 ? 64 : 70); + final int tagWidth = phase == 3 ? 82 : (phase == 1 ? 68 : 74); + final int accountStatusWidth = phase == 2 ? 52 : 44; + final int accountDeleteWidth = phase == 3 ? 26 : 22; + final String nameSpacing = + phase == 2 ? '0.14px' : (phase == 1 ? '0.08px' : '0px'); + final String descriptionSpacing = phase == 3 ? '0.12px' : '0px'; + final String accountNameSpacing = + phase == 2 ? '0.10px' : (phase == 1 ? '0.05px' : '0px'); + + for (int index = 0; index < sections.length; index++) { + final dom.Element section = sections[index] as dom.Element; + final bool expanded = (iteration + index) % 3 != 1; + section.className = expanded ? 'otc-section expanded' : 'otc-section collapsed'; + section.setInlineStyle('gap', phase == 2 ? '11px' : '12px'); + } + + for (int index = 0; index < cards.length; index++) { + final dom.Element card = cards[index] as dom.Element; + final bool extra = card.getAttribute('data-extra') == 'true'; + final bool selected = (index + phase) % 5 == 0; + final bool expanded = selected || (index + iteration) % 3 != 1; + final bool disabled = (index + phase) % 7 == 3; + final List classNames = ['otc-card']; + if (extra) { + classNames.add('extra'); + } + classNames.add(expanded ? 'expanded' : 'collapsed'); + if (selected) { + classNames.add('selected'); + } + if (disabled) { + classNames.add('disabled'); + } + card.className = classNames.join(' '); + card.setInlineStyle('order', '${(index * 5 + phase * 3) % cards.length}'); + } + + for (final dynamic node in headers) { + final dom.Element element = node as dom.Element; + element.setInlineStyle( + 'padding', + '${headerPaddingBlock}px ${headerPaddingInline}px', + ); + } + for (final dynamic node in leadings) { + (node as dom.Element).setInlineStyle('gap', '${leadingGap}px'); + } + for (int index = 0; index < copies.length; index++) { + final dom.Element element = copies[index] as dom.Element; + final int variance = index.isEven ? 0 : (index % 3 == 0 ? 10 : -8); + element.setInlineStyle('width', '${copyWidth + variance}px'); + } + for (final dynamic node in nameRows) { + (node as dom.Element).setInlineStyle('gap', phase == 1 ? '10px' : '8px'); + } + for (final dynamic node in names) { + (node as dom.Element).setInlineStyle('letterSpacing', nameSpacing); + } + for (final dynamic node in descriptions) { + final dom.Element element = node as dom.Element; + element.setInlineStyle('letterSpacing', descriptionSpacing); + element.setInlineStyle('wordSpacing', phase == 2 ? '0.20px' : '0px'); + } + for (final dynamic node in prices) { + (node as dom.Element).setInlineStyle('width', '${priceWidth}px'); + } + for (final dynamic node in tags) { + (node as dom.Element).setInlineStyle('width', '${tagWidth}px'); + } + for (int index = 0; index < accountRows.length; index++) { + final dom.Element row = accountRows[index] as dom.Element; + final bool selected = (index + iteration) % 4 == 0; + row.className = selected ? 'otc-account-row selected' : 'otc-account-row'; + row.setInlineStyle('paddingTop', phase == 1 ? '10px' : '8px'); + } + for (final dynamic node in accountBoxes) { + (node as dom.Element).setInlineStyle('width', 'calc(100% - 32px)'); + } + for (final dynamic node in accountItems) { + (node as dom.Element).setInlineStyle('gap', phase == 2 ? '10px' : '8px'); + } + for (final dynamic node in accountMains) { + (node as dom.Element).setInlineStyle('gap', phase == 1 ? '10px' : '8px'); + } + for (int index = 0; index < accountCopies.length; index++) { + final dom.Element element = accountCopies[index] as dom.Element; + final int variance = index.isEven ? 0 : (index % 3 == 0 ? 6 : -6); + element.setInlineStyle('paddingRight', '${phase == 2 ? 2 : 0}px'); + element.setInlineStyle('width', 'calc(100% - ${accountStatusWidth + accountDeleteWidth + 18 + variance}px)'); + } + for (final dynamic node in accountNames) { + (node as dom.Element).setInlineStyle('letterSpacing', accountNameSpacing); + } + for (final dynamic node in accountDescriptions) { + final dom.Element element = node as dom.Element; + element.setInlineStyle('letterSpacing', phase == 3 ? '0.10px' : '0px'); + element.setInlineStyle('wordSpacing', phase == 2 ? '0.18px' : '0px'); + } + for (final dynamic node in accountStatuses) { + (node as dom.Element).setInlineStyle('width', '${accountStatusWidth}px'); + } + for (final dynamic node in accountDeletes) { + (node as dom.Element).setInlineStyle('width', '${accountDeleteWidth}px'); + } + for (final dynamic node in addAccounts) { + (node as dom.Element).setInlineStyle( + 'minHeight', + phase == 2 ? '50px' : '48px', + ); + } + for (final dynamic node in showMoreRows) { + (node as dom.Element).setInlineStyle( + 'paddingTop', + phase == 1 ? '3px' : '0px', + ); + } + + sheet.style.flushPendingProperties(); + sheet.ownerDocument.updateStyleIfNeeded(); + await _pumpFrames(prepared.tester, 3); + } +} + +Future _runFlexInlineLayoutLoop( + _PreparedProfileCase prepared, { + required int mutationIterations, + required List widths, +}) async { + final dom.Element host = prepared.getElementById('host'); + final dom.Element board = prepared.getElementById('board'); + final List fastlaneA = board.querySelectorAll(['.fastlane-a']); + final List fastlaneB = board.querySelectorAll(['.fastlane-b']); + final List fastlaneD = board.querySelectorAll(['.fastlane-d']); + final List handoffA = board.querySelectorAll(['.handoff-a']); + final List handoffD = board.querySelectorAll(['.handoff-d']); + final List ribbonB = board.querySelectorAll(['.ribbon-b']); + final List trailA = board.querySelectorAll(['.trail-a']); + for (int iteration = 0; iteration < mutationIterations; iteration++) { + final int phase = iteration % widths.length; + host.setInlineStyle('width', widths[phase]); + host.setInlineStyle('padding', phase.isEven ? '10px' : '8px'); + board.className = 'phase-$phase'; + final String fastlaneAWidth = + phase == 1 ? '58px' : (phase == 2 ? '56px' : (phase == 3 ? '60px' : '52px')); + final String fastlaneBWidth = + phase == 1 || phase == 3 ? '52px' : '48px'; + final String fastlaneDWidth = phase == 2 ? '50px' : '46px'; + final String handoffAWidth = + phase == 1 || phase == 3 ? '54px' : '50px'; + final String handoffDWidth = + phase == 3 ? '62px' : ((phase == 1 || phase == 2) ? '60px' : '56px'); + final String ribbonBWidth = phase == 1 ? '52px' : '48px'; + final String trailAWidth = phase == 3 ? '58px' : '54px'; + for (final dynamic node in fastlaneA) { + (node as dom.Element).setInlineStyle('width', fastlaneAWidth); + } + for (final dynamic node in fastlaneB) { + (node as dom.Element).setInlineStyle('width', fastlaneBWidth); + } + for (final dynamic node in fastlaneD) { + (node as dom.Element).setInlineStyle('width', fastlaneDWidth); + } + for (final dynamic node in handoffA) { + (node as dom.Element).setInlineStyle('width', handoffAWidth); + } + for (final dynamic node in handoffD) { + (node as dom.Element).setInlineStyle('width', handoffDWidth); + } + for (final dynamic node in ribbonB) { + (node as dom.Element).setInlineStyle('width', ribbonBWidth); + } + for (final dynamic node in trailA) { + (node as dom.Element).setInlineStyle('width', trailAWidth); + } + board.setInlineStyle('letterSpacing', phase == 1 ? '0.12px' : '0px'); + board.setInlineStyle('wordSpacing', phase == 2 ? '0.35px' : '0px'); + board.style.flushPendingProperties(); + board.ownerDocument.updateStyleIfNeeded(); + await _pumpFrames(prepared.tester, 2); + } +} + +Future _runFlexAdjustFastPathLoop( + _PreparedProfileCase prepared, { + required int mutationIterations, + required List widths, +}) async { + final dom.Element host = prepared.getElementById('host'); + final dom.Element board = prepared.getElementById('board'); + final List cards = board.querySelectorAll(['.card']); + final List severities = board.querySelectorAll(['.severity']); + final List lanes = board.querySelectorAll(['.lane']); + final List badges = board.querySelectorAll(['.badge']); + final List owners = board.querySelectorAll(['.owner']); + final List etas = board.querySelectorAll(['.eta']); + final List subtleChips = board.querySelectorAll(['.chip.subtle']); + final List pickers = board.querySelectorAll(['.picker']); + final List routeSelects = board.querySelectorAll(['.route-select']); + final List miniActions = board.querySelectorAll(['.mini-action']); + final List copyBoxes = board.querySelectorAll(['.copy-box']); + final List noteBoxes = board.querySelectorAll(['.note-box']); + final List packBoxes = board.querySelectorAll(['.pack-box']); + final List groupBoxes = board.querySelectorAll(['.group-box']); + final List segTags = board.querySelectorAll(['.seg-tag']); + final List segCopies = board.querySelectorAll(['.seg-copy']); + final List segNotes = board.querySelectorAll(['.seg-note']); + final List segTails = board.querySelectorAll(['.seg-tail']); + final List autoBodies = board.querySelectorAll(['.auto-copy']); + final List autoNotes = board.querySelectorAll(['.auto-note']); + for (int iteration = 0; iteration < mutationIterations; iteration++) { + final int phase = iteration % widths.length; + host.setInlineStyle('width', widths[phase]); + host.setInlineStyle('padding', phase.isEven ? '10px' : '8px'); + board.setInlineStyle('gap', phase == 2 ? '7px' : '8px'); + final int cardMinWidth = + phase == 0 ? 144 : (phase == 1 ? 136 : (phase == 2 ? 152 : 140)); + final int cardMaxWidth = + phase == 0 ? 170 : (phase == 1 ? 160 : (phase == 2 ? 178 : 166)); + final int severityWidth = phase == 2 ? 36 : 34; + final int laneWidth = phase == 1 ? 46 : (phase == 2 ? 50 : 44); + final int badgeWidth = phase == 3 ? 38 : 36; + final int ownerWidth = phase == 2 ? 54 : 50; + final int etaWidth = phase == 1 ? 44 : 42; + final int subtleChipWidth = phase == 3 ? 38 : 34; + final int pickerWidth = phase == 2 ? 78 : (phase == 1 ? 68 : 72); + final int routeSelectWidth = phase == 3 ? 74 : (phase == 2 ? 70 : 66); + final int miniActionWidth = phase == 1 ? 42 : 38; + final int copyBoxWidth = phase == 2 ? 76 : (phase == 1 ? 62 : (phase == 3 ? 70 : 66)); + final int noteBoxWidth = phase == 3 ? 60 : (phase == 2 ? 56 : 52); + final int packBoxWidth = phase == 2 ? 42 : (phase == 1 ? 32 : 36); + final int groupBoxWidth = phase == 2 ? 78 : (phase == 1 ? 64 : (phase == 3 ? 72 : 68)); + final int segTagWidth = phase == 1 ? 16 : 14; + final int segCopyWidth = phase == 2 ? 40 : (phase == 1 ? 32 : 36); + final int segNoteWidth = phase == 3 ? 32 : (phase == 2 ? 30 : 28); + final int segTailWidth = phase == 2 ? 14 : 12; + final String bodySpacing = phase == 1 ? '0.14px' : (phase == 2 ? '0.28px' : '0px'); + final String noteSpacing = phase == 3 ? '0.22px' : '0px'; + final String packGap = phase == 2 ? '3px' : '2px'; + final String groupGap = phase == 2 ? '3px' : '2px'; + for (int index = 0; index < cards.length; index++) { + final dom.Element element = cards[index] as dom.Element; + final int variance = index % 3 == 0 ? 0 : (index % 3 == 1 ? -4 : 4); + element.setInlineStyle('minWidth', '${cardMinWidth + variance}px'); + element.setInlineStyle('maxWidth', '${cardMaxWidth + variance}px'); + } + for (final dynamic node in severities) { + (node as dom.Element).setInlineStyle('width', '${severityWidth}px'); + } + for (final dynamic node in lanes) { + (node as dom.Element).setInlineStyle('width', '${laneWidth}px'); + } + for (final dynamic node in badges) { + (node as dom.Element).setInlineStyle('width', '${badgeWidth}px'); + } + for (final dynamic node in owners) { + (node as dom.Element).setInlineStyle('width', '${ownerWidth}px'); + } + for (final dynamic node in etas) { + (node as dom.Element).setInlineStyle('width', '${etaWidth}px'); + } + for (final dynamic node in subtleChips) { + (node as dom.Element).setInlineStyle('width', '${subtleChipWidth}px'); + } + for (final dynamic node in pickers) { + (node as dom.Element).setInlineStyle('width', '${pickerWidth}px'); + } + for (final dynamic node in routeSelects) { + (node as dom.Element).setInlineStyle('width', '${routeSelectWidth}px'); + } + for (final dynamic node in miniActions) { + (node as dom.Element).setInlineStyle('width', '${miniActionWidth}px'); + } + for (int index = 0; index < copyBoxes.length; index++) { + final dom.Element element = copyBoxes[index] as dom.Element; + final int variance = index.isEven ? 0 : (index % 3 == 0 ? 6 : -4); + element.setInlineStyle('width', '${copyBoxWidth + variance}px'); + element.setInlineStyle('letterSpacing', bodySpacing); + } + for (int index = 0; index < noteBoxes.length; index++) { + final dom.Element element = noteBoxes[index] as dom.Element; + final int variance = index.isEven ? 0 : (index % 3 == 0 ? 4 : -2); + element.setInlineStyle('width', '${noteBoxWidth + variance}px'); + element.setInlineStyle('letterSpacing', noteSpacing); + } + for (int index = 0; index < packBoxes.length; index++) { + final dom.Element element = packBoxes[index] as dom.Element; + final int variance = index.isEven ? 0 : (index % 3 == 0 ? 2 : -2); + element.setInlineStyle('width', '${packBoxWidth + variance}px'); + element.setInlineStyle('gap', packGap); + } + for (int index = 0; index < groupBoxes.length; index++) { + final dom.Element element = groupBoxes[index] as dom.Element; + final int variance = index.isEven ? 0 : (index % 3 == 0 ? 4 : -2); + element.setInlineStyle('width', '${groupBoxWidth + variance}px'); + element.setInlineStyle('gap', groupGap); + } + for (int index = 0; index < segTags.length; index++) { + final dom.Element element = segTags[index] as dom.Element; + final int variance = index.isEven ? 0 : (index % 3 == 0 ? 2 : 0); + element.setInlineStyle('width', '${segTagWidth + variance}px'); + } + for (int index = 0; index < segCopies.length; index++) { + final dom.Element element = segCopies[index] as dom.Element; + final int variance = index.isEven ? 0 : (index % 3 == 0 ? 4 : -2); + element.setInlineStyle('width', '${segCopyWidth + variance}px'); + element.setInlineStyle('letterSpacing', bodySpacing); + } + for (int index = 0; index < segNotes.length; index++) { + final dom.Element element = segNotes[index] as dom.Element; + final int variance = index.isEven ? 0 : (index % 3 == 0 ? 2 : -2); + element.setInlineStyle('width', '${segNoteWidth + variance}px'); + element.setInlineStyle('letterSpacing', noteSpacing); + } + for (final dynamic node in segTails) { + (node as dom.Element).setInlineStyle('width', '${segTailWidth}px'); + } + for (final dynamic node in autoBodies) { + final dom.Element element = node as dom.Element; + element.setInlineStyle('letterSpacing', bodySpacing); + element.setInlineStyle('wordSpacing', phase == 2 ? '0.35px' : '0px'); + } + for (final dynamic node in autoNotes) { + final dom.Element element = node as dom.Element; + element.setInlineStyle('letterSpacing', noteSpacing); + } + board.style.flushPendingProperties(); + board.ownerDocument.updateStyleIfNeeded(); + await _pumpFrames(prepared.tester, 3); + } +} + +Future _runFlexRunMetricsDenseLoop( + _PreparedProfileCase prepared, { + required int mutationIterations, + required List widths, +}) async { + final dom.Element host = prepared.getElementById('host'); + final dom.Element board = prepared.getElementById('board'); + final List cards = board.querySelectorAll(['.card']); + final List severities = board.querySelectorAll(['.severity']); + final List lanes = board.querySelectorAll(['.lane']); + final List badges = board.querySelectorAll(['.badge']); + final List owners = board.querySelectorAll(['.owner']); + final List etas = board.querySelectorAll(['.eta']); + final List subtleChips = board.querySelectorAll(['.chip.subtle']); + final List autoBodies = board.querySelectorAll(['.auto-copy']); + final List autoNotes = board.querySelectorAll(['.auto-note']); + final List autoPacks = board.querySelectorAll(['.auto-pack']); + final List toolInputs = board.querySelectorAll(['.tool-input']); + final List toolSelects = board.querySelectorAll(['.tool-select']); + final List miniActions = board.querySelectorAll(['.mini-action']); + final List summaryCopies = board.querySelectorAll(['.summary-copy']); + for (int iteration = 0; iteration < mutationIterations; iteration++) { + final int phase = iteration % widths.length; + host.setInlineStyle('width', widths[phase]); + host.setInlineStyle('padding', phase.isEven ? '10px' : '8px'); + board.setInlineStyle('gap', phase == 2 ? '7px' : '8px'); + final int cardMinWidth = + phase == 0 ? 168 : (phase == 1 ? 160 : (phase == 2 ? 176 : 164)); + final int cardMaxWidth = + phase == 0 ? 192 : (phase == 1 ? 184 : (phase == 2 ? 200 : 188)); + final int severityWidth = phase == 2 ? 36 : 34; + final int laneWidth = phase == 1 ? 46 : (phase == 2 ? 50 : 44); + final int badgeWidth = phase == 3 ? 38 : 36; + final int ownerWidth = phase == 2 ? 54 : 50; + final int etaWidth = phase == 1 ? 44 : 42; + final int subtleChipWidth = phase == 3 ? 38 : 34; + final int toolInputWidth = phase == 2 ? 66 : (phase == 1 ? 56 : 60); + final int toolSelectWidth = phase == 3 ? 60 : (phase == 2 ? 64 : 56); + final int miniActionWidth = phase == 1 ? 40 : 36; + final String bodySpacing = phase == 1 ? '0.06px' : (phase == 2 ? '0.12px' : '0px'); + final String noteSpacing = phase == 3 ? '0.08px' : '0px'; + final String summarySpacing = phase == 2 ? '0.10px' : '0px'; + final String packGap = phase == 2 ? '4px' : '3px'; + for (int index = 0; index < cards.length; index++) { + final dom.Element element = cards[index] as dom.Element; + final int variance = index % 3 == 0 ? 0 : (index % 3 == 1 ? -4 : 4); + element.setInlineStyle('minWidth', '${cardMinWidth + variance}px'); + element.setInlineStyle('maxWidth', '${cardMaxWidth + variance}px'); + } + for (final dynamic node in severities) { + (node as dom.Element).setInlineStyle('width', '${severityWidth}px'); + } + for (final dynamic node in lanes) { + (node as dom.Element).setInlineStyle('width', '${laneWidth}px'); + } + for (final dynamic node in badges) { + (node as dom.Element).setInlineStyle('width', '${badgeWidth}px'); + } + for (final dynamic node in owners) { + (node as dom.Element).setInlineStyle('width', '${ownerWidth}px'); + } + for (final dynamic node in etas) { + (node as dom.Element).setInlineStyle('width', '${etaWidth}px'); + } + for (final dynamic node in subtleChips) { + (node as dom.Element).setInlineStyle('width', '${subtleChipWidth}px'); + } + for (final dynamic node in toolInputs) { + (node as dom.Element).setInlineStyle('width', '${toolInputWidth}px'); + } + for (final dynamic node in toolSelects) { + (node as dom.Element).setInlineStyle('width', '${toolSelectWidth}px'); + } + for (final dynamic node in miniActions) { + (node as dom.Element).setInlineStyle('width', '${miniActionWidth}px'); + } + for (final dynamic node in autoBodies) { + final dom.Element element = node as dom.Element; + element.setInlineStyle('letterSpacing', bodySpacing); + element.setInlineStyle('wordSpacing', '0px'); + } + for (final dynamic node in autoNotes) { + final dom.Element element = node as dom.Element; + element.setInlineStyle('letterSpacing', noteSpacing); + } + for (final dynamic node in summaryCopies) { + (node as dom.Element).setInlineStyle('letterSpacing', summarySpacing); + } + for (final dynamic node in autoPacks) { + (node as dom.Element).setInlineStyle('gap', packGap); + } + board.style.flushPendingProperties(); + board.ownerDocument.updateStyleIfNeeded(); + await _pumpFrames(prepared.tester, 3); + } +} + +Future _runFlexAdjustWidgetDenseLoop( + _PreparedProfileCase prepared, { + required int mutationIterations, + required List widths, +}) async { + final dom.Element host = prepared.getElementById('host'); + final dom.Element board = prepared.getElementById('board'); + final List cards = board.querySelectorAll(['.card']); + final List severities = board.querySelectorAll(['.severity']); + final List lanes = board.querySelectorAll(['.lane']); + final List badges = board.querySelectorAll(['.badge']); + final List owners = board.querySelectorAll(['.owner']); + final List etas = board.querySelectorAll(['.eta']); + final List subtleChips = board.querySelectorAll(['.chip.subtle']); + final List toolInputA = board.querySelectorAll(['.tool-input-a']); + final List toolInputB = board.querySelectorAll(['.tool-input-b']); + final List toolSelectA = board.querySelectorAll(['.tool-select-a']); + final List toolSelectB = board.querySelectorAll(['.tool-select-b']); + final List toolChips = board.querySelectorAll(['.tool-chip']); + final List autoBodies = board.querySelectorAll(['.auto-copy']); + final List autoNotes = board.querySelectorAll(['.auto-note']); + final List summaryCopies = board.querySelectorAll(['.summary-copy']); + for (int iteration = 0; iteration < mutationIterations; iteration++) { + final int phase = iteration % widths.length; + host.setInlineStyle('width', widths[phase]); + host.setInlineStyle('padding', phase.isEven ? '10px' : '8px'); + board.setInlineStyle('gap', phase == 2 ? '7px' : '8px'); + final int cardMinWidth = + phase == 0 ? 152 : (phase == 1 ? 144 : (phase == 2 ? 160 : 148)); + final int cardMaxWidth = + phase == 0 ? 178 : (phase == 1 ? 170 : (phase == 2 ? 186 : 174)); + final int severityWidth = phase == 2 ? 36 : 34; + final int laneWidth = phase == 1 ? 46 : (phase == 2 ? 50 : 44); + final int badgeWidth = phase == 3 ? 38 : 36; + final int ownerWidth = phase == 2 ? 54 : 50; + final int etaWidth = phase == 1 ? 44 : 42; + final int subtleChipWidth = phase == 3 ? 38 : 34; + final int inputAWidth = phase == 2 ? 78 : (phase == 1 ? 68 : 72); + final int inputBWidth = phase == 3 ? 66 : (phase == 2 ? 62 : 58); + final int selectAWidth = phase == 3 ? 74 : (phase == 2 ? 70 : 66); + final int selectBWidth = phase == 1 ? 62 : (phase == 2 ? 66 : 58); + final int toolChipWidth = phase == 2 ? 36 : 32; + final String bodySpacing = + phase == 1 ? '0.10px' : (phase == 2 ? '0.18px' : '0px'); + final String noteSpacing = phase == 3 ? '0.12px' : '0px'; + final String summarySpacing = phase == 2 ? '0.14px' : '0px'; + for (int index = 0; index < cards.length; index++) { + final dom.Element element = cards[index] as dom.Element; + final int variance = index % 3 == 0 ? 0 : (index % 3 == 1 ? -4 : 4); + element.setInlineStyle('minWidth', '${cardMinWidth + variance}px'); + element.setInlineStyle('maxWidth', '${cardMaxWidth + variance}px'); + } + for (final dynamic node in severities) { + (node as dom.Element).setInlineStyle('width', '${severityWidth}px'); + } + for (final dynamic node in lanes) { + (node as dom.Element).setInlineStyle('width', '${laneWidth}px'); + } + for (final dynamic node in badges) { + (node as dom.Element).setInlineStyle('width', '${badgeWidth}px'); + } + for (final dynamic node in owners) { + (node as dom.Element).setInlineStyle('width', '${ownerWidth}px'); + } + for (final dynamic node in etas) { + (node as dom.Element).setInlineStyle('width', '${etaWidth}px'); + } + for (final dynamic node in subtleChips) { + (node as dom.Element).setInlineStyle('width', '${subtleChipWidth}px'); + } + for (final dynamic node in toolInputA) { + (node as dom.Element).setInlineStyle('width', '${inputAWidth}px'); + } + for (final dynamic node in toolInputB) { + (node as dom.Element).setInlineStyle('width', '${inputBWidth}px'); + } + for (final dynamic node in toolSelectA) { + (node as dom.Element).setInlineStyle('width', '${selectAWidth}px'); + } + for (final dynamic node in toolSelectB) { + (node as dom.Element).setInlineStyle('width', '${selectBWidth}px'); + } + for (final dynamic node in toolChips) { + (node as dom.Element).setInlineStyle('width', '${toolChipWidth}px'); + } + for (final dynamic node in autoBodies) { + final dom.Element element = node as dom.Element; + element.setInlineStyle('letterSpacing', bodySpacing); + element.setInlineStyle('wordSpacing', phase == 2 ? '0.24px' : '0px'); + } + for (final dynamic node in autoNotes) { + (node as dom.Element).setInlineStyle('letterSpacing', noteSpacing); + } + for (final dynamic node in summaryCopies) { + (node as dom.Element).setInlineStyle('letterSpacing', summarySpacing); + } + board.style.flushPendingProperties(); + board.ownerDocument.updateStyleIfNeeded(); + await _pumpFrames(prepared.tester, 3); + } +} + +Future _runFlexTightFastPathDenseLoop( + _PreparedProfileCase prepared, { + required int mutationIterations, + required List widths, +}) async { + final dom.Element host = prepared.getElementById('host'); + final dom.Element board = prepared.getElementById('board'); + final List cards = board.querySelectorAll(['.card']); + final List severities = board.querySelectorAll(['.severity']); + final List lanes = board.querySelectorAll(['.lane']); + final List badges = board.querySelectorAll(['.badge']); + final List owners = board.querySelectorAll(['.owner']); + final List etas = board.querySelectorAll(['.eta']); + final List subtleChips = board.querySelectorAll(['.chip.subtle']); + final List tightCopies = board.querySelectorAll(['.tight-copy']); + final List tightNotes = board.querySelectorAll(['.tight-note']); + final List tightPacks = board.querySelectorAll(['.tight-pack']); + final List toolInputA = board.querySelectorAll(['.tool-input-a']); + final List toolInputB = board.querySelectorAll(['.tool-input-b']); + final List toolSelectA = board.querySelectorAll(['.tool-select-a']); + final List toolSelectB = board.querySelectorAll(['.tool-select-b']); + final List miniActions = board.querySelectorAll(['.mini-action']); + final List summaryCopies = board.querySelectorAll(['.summary-copy']); + for (int iteration = 0; iteration < mutationIterations; iteration++) { + final int phase = iteration % widths.length; + host.setInlineStyle('width', widths[phase]); + host.setInlineStyle('padding', phase.isEven ? '10px' : '8px'); + board.setInlineStyle('gap', phase == 2 ? '7px' : '8px'); + final int cardMinWidth = + phase == 0 ? 164 : (phase == 1 ? 156 : (phase == 2 ? 172 : 160)); + final int cardMaxWidth = + phase == 0 ? 190 : (phase == 1 ? 182 : (phase == 2 ? 198 : 186)); + final int severityWidth = phase == 2 ? 36 : 34; + final int laneWidth = phase == 1 ? 46 : (phase == 2 ? 50 : 44); + final int badgeWidth = phase == 3 ? 38 : 36; + final int ownerWidth = phase == 2 ? 54 : 50; + final int etaWidth = phase == 1 ? 44 : 42; + final int subtleChipWidth = phase == 3 ? 38 : 34; + final int tightCopyWidth = phase == 2 ? 70 : (phase == 1 ? 58 : 64); + final int tightNoteWidth = phase == 3 ? 54 : (phase == 2 ? 50 : 46); + final int tightPackWidth = phase == 2 ? 42 : (phase == 1 ? 34 : 38); + final int inputAWidth = phase == 2 ? 74 : (phase == 1 ? 66 : 70); + final int inputBWidth = phase == 3 ? 64 : (phase == 2 ? 60 : 56); + final int selectAWidth = phase == 3 ? 70 : (phase == 2 ? 66 : 62); + final int selectBWidth = phase == 1 ? 64 : (phase == 2 ? 68 : 60); + final int miniActionWidth = phase == 1 ? 40 : 36; + final String summarySpacing = phase == 2 ? '0.12px' : '0px'; + for (int index = 0; index < cards.length; index++) { + final dom.Element element = cards[index] as dom.Element; + final int variance = index % 3 == 0 ? 0 : (index % 3 == 1 ? -4 : 4); + element.setInlineStyle('minWidth', '${cardMinWidth + variance}px'); + element.setInlineStyle('maxWidth', '${cardMaxWidth + variance}px'); + } + for (final dynamic node in severities) { + (node as dom.Element).setInlineStyle('width', '${severityWidth}px'); + } + for (final dynamic node in lanes) { + (node as dom.Element).setInlineStyle('width', '${laneWidth}px'); + } + for (final dynamic node in badges) { + (node as dom.Element).setInlineStyle('width', '${badgeWidth}px'); + } + for (final dynamic node in owners) { + (node as dom.Element).setInlineStyle('width', '${ownerWidth}px'); + } + for (final dynamic node in etas) { + (node as dom.Element).setInlineStyle('width', '${etaWidth}px'); + } + for (final dynamic node in subtleChips) { + (node as dom.Element).setInlineStyle('width', '${subtleChipWidth}px'); + } + for (int index = 0; index < tightCopies.length; index++) { + final dom.Element element = tightCopies[index] as dom.Element; + final int variance = index.isEven ? 0 : (index % 3 == 0 ? 4 : -4); + element.setInlineStyle('flexBasis', '${tightCopyWidth + variance}px'); + } + for (int index = 0; index < tightNotes.length; index++) { + final dom.Element element = tightNotes[index] as dom.Element; + final int variance = index.isEven ? 0 : (index % 3 == 0 ? 2 : -2); + element.setInlineStyle('flexBasis', '${tightNoteWidth + variance}px'); + } + for (int index = 0; index < tightPacks.length; index++) { + final dom.Element element = tightPacks[index] as dom.Element; + final int variance = index.isEven ? 0 : (index % 3 == 0 ? 2 : -2); + element.setInlineStyle('flexBasis', '${tightPackWidth + variance}px'); + } + for (final dynamic node in toolInputA) { + (node as dom.Element).setInlineStyle('width', '${inputAWidth}px'); + } + for (final dynamic node in toolInputB) { + (node as dom.Element).setInlineStyle('width', '${inputBWidth}px'); + } + for (final dynamic node in toolSelectA) { + (node as dom.Element).setInlineStyle('width', '${selectAWidth}px'); + } + for (final dynamic node in toolSelectB) { + (node as dom.Element).setInlineStyle('width', '${selectBWidth}px'); + } + for (final dynamic node in miniActions) { + (node as dom.Element).setInlineStyle('width', '${miniActionWidth}px'); + } + for (final dynamic node in summaryCopies) { + (node as dom.Element).setInlineStyle('letterSpacing', summarySpacing); + } + board.style.flushPendingProperties(); + board.ownerDocument.updateStyleIfNeeded(); + await _pumpFrames(prepared.tester, 3); + } +} + +Future _runFlexHybridFastPathDenseLoop( + _PreparedProfileCase prepared, { + required int mutationIterations, + required List widths, +}) async { + final dom.Element host = prepared.getElementById('host'); + final dom.Element board = prepared.getElementById('board'); + final List cards = board.querySelectorAll(['.card']); + final List severities = board.querySelectorAll(['.severity']); + final List lanes = board.querySelectorAll(['.lane']); + final List badges = board.querySelectorAll(['.badge']); + final List owners = board.querySelectorAll(['.owner']); + final List etas = board.querySelectorAll(['.eta']); + final List subtleChips = board.querySelectorAll(['.chip.subtle']); + final List autoBodies = board.querySelectorAll(['.auto-copy']); + final List autoNotes = board.querySelectorAll(['.auto-note']); + final List autoPacks = board.querySelectorAll(['.auto-pack']); + final List summaryCopies = board.querySelectorAll(['.summary-copy']); + final List toolInputs = board.querySelectorAll(['.tool-input']); + final List toolSelects = board.querySelectorAll(['.tool-select']); + final List miniActions = board.querySelectorAll(['.mini-action']); + final List tightCopies = board.querySelectorAll(['.tight-copy']); + final List tightNotes = board.querySelectorAll(['.tight-note']); + final List tightPacks = board.querySelectorAll(['.tight-pack']); + final List tightInputA = board.querySelectorAll(['.tight-input-a']); + final List tightInputB = board.querySelectorAll(['.tight-input-b']); + final List tightSelectA = board.querySelectorAll(['.tight-select-a']); + final List tightSelectB = board.querySelectorAll(['.tight-select-b']); + final List miniTight = board.querySelectorAll(['.mini-tight']); + for (int iteration = 0; iteration < mutationIterations; iteration++) { + final int phase = iteration % widths.length; + host.setInlineStyle('width', widths[phase]); + host.setInlineStyle('padding', phase.isEven ? '10px' : '8px'); + board.setInlineStyle('gap', phase == 2 ? '7px' : '8px'); + final int cardMinWidth = + phase == 0 ? 166 : (phase == 1 ? 158 : (phase == 2 ? 174 : 162)); + final int cardMaxWidth = + phase == 0 ? 190 : (phase == 1 ? 182 : (phase == 2 ? 198 : 186)); + final int severityWidth = phase == 2 ? 36 : 34; + final int laneWidth = phase == 1 ? 46 : (phase == 2 ? 50 : 44); + final int badgeWidth = phase == 3 ? 38 : 36; + final int ownerWidth = phase == 2 ? 54 : 50; + final int etaWidth = phase == 1 ? 44 : 42; + final int subtleChipWidth = phase == 3 ? 38 : 34; + final int toolInputWidth = phase == 2 ? 66 : (phase == 1 ? 56 : 60); + final int toolSelectWidth = phase == 3 ? 60 : (phase == 2 ? 64 : 56); + final int miniActionWidth = phase == 1 ? 40 : 36; + final int tightCopyWidth = phase == 2 ? 66 : (phase == 1 ? 54 : 60); + final int tightNoteWidth = phase == 3 ? 52 : (phase == 2 ? 48 : 44); + final int tightPackWidth = phase == 2 ? 40 : (phase == 1 ? 32 : 36); + final int tightInputAWidth = phase == 2 ? 72 : (phase == 1 ? 60 : 66); + final int tightInputBWidth = phase == 3 ? 64 : (phase == 2 ? 58 : 54); + final int tightSelectAWidth = phase == 3 ? 70 : (phase == 2 ? 64 : 60); + final int tightSelectBWidth = phase == 1 ? 62 : (phase == 2 ? 66 : 58); + final int miniTightWidth = phase == 1 ? 38 : 34; + final String bodySpacing = + phase == 1 ? '0.06px' : (phase == 2 ? '0.12px' : '0px'); + final String noteSpacing = phase == 3 ? '0.08px' : '0px'; + final String summarySpacing = phase == 2 ? '0.10px' : '0px'; + final String packGap = phase == 2 ? '4px' : '3px'; + for (int index = 0; index < cards.length; index++) { + final dom.Element element = cards[index] as dom.Element; + final int variance = index % 3 == 0 ? 0 : (index % 3 == 1 ? -4 : 4); + element.setInlineStyle('minWidth', '${cardMinWidth + variance}px'); + element.setInlineStyle('maxWidth', '${cardMaxWidth + variance}px'); + } + for (final dynamic node in severities) { + (node as dom.Element).setInlineStyle('width', '${severityWidth}px'); + } + for (final dynamic node in lanes) { + (node as dom.Element).setInlineStyle('width', '${laneWidth}px'); + } + for (final dynamic node in badges) { + (node as dom.Element).setInlineStyle('width', '${badgeWidth}px'); + } + for (final dynamic node in owners) { + (node as dom.Element).setInlineStyle('width', '${ownerWidth}px'); + } + for (final dynamic node in etas) { + (node as dom.Element).setInlineStyle('width', '${etaWidth}px'); + } + for (final dynamic node in subtleChips) { + (node as dom.Element).setInlineStyle('width', '${subtleChipWidth}px'); + } + for (final dynamic node in toolInputs) { + (node as dom.Element).setInlineStyle('width', '${toolInputWidth}px'); + } + for (final dynamic node in toolSelects) { + (node as dom.Element).setInlineStyle('width', '${toolSelectWidth}px'); + } + for (final dynamic node in miniActions) { + (node as dom.Element).setInlineStyle('width', '${miniActionWidth}px'); + } + for (final dynamic node in autoBodies) { + final dom.Element element = node as dom.Element; + element.setInlineStyle('letterSpacing', bodySpacing); + element.setInlineStyle('wordSpacing', phase == 2 ? '0.24px' : '0px'); + } + for (final dynamic node in autoNotes) { + (node as dom.Element).setInlineStyle('letterSpacing', noteSpacing); + } + for (final dynamic node in autoPacks) { + (node as dom.Element).setInlineStyle('gap', packGap); + } + for (final dynamic node in summaryCopies) { + (node as dom.Element).setInlineStyle('letterSpacing', summarySpacing); + } + for (int index = 0; index < tightCopies.length; index++) { + final dom.Element element = tightCopies[index] as dom.Element; + final int variance = index.isEven ? 0 : (index % 3 == 0 ? 4 : -4); + element.setInlineStyle('flexBasis', '${tightCopyWidth + variance}px'); + } + for (int index = 0; index < tightNotes.length; index++) { + final dom.Element element = tightNotes[index] as dom.Element; + final int variance = index.isEven ? 0 : (index % 3 == 0 ? 2 : -2); + element.setInlineStyle('flexBasis', '${tightNoteWidth + variance}px'); + } + for (int index = 0; index < tightPacks.length; index++) { + final dom.Element element = tightPacks[index] as dom.Element; + final int variance = index.isEven ? 0 : (index % 3 == 0 ? 2 : -2); + element.setInlineStyle('flexBasis', '${tightPackWidth + variance}px'); + } + for (final dynamic node in tightInputA) { + (node as dom.Element).setInlineStyle('width', '${tightInputAWidth}px'); + } + for (final dynamic node in tightInputB) { + (node as dom.Element).setInlineStyle('width', '${tightInputBWidth}px'); + } + for (final dynamic node in tightSelectA) { + (node as dom.Element).setInlineStyle('width', '${tightSelectAWidth}px'); + } + for (final dynamic node in tightSelectB) { + (node as dom.Element).setInlineStyle('width', '${tightSelectBWidth}px'); + } + for (final dynamic node in miniTight) { + (node as dom.Element).setInlineStyle('width', '${miniTightWidth}px'); + } + board.style.flushPendingProperties(); + board.ownerDocument.updateStyleIfNeeded(); + await _pumpFrames(prepared.tester, 3); + } +} + +Future _pumpFrames( + WidgetTester tester, + int frames, { + Duration frameDuration = const Duration(milliseconds: 16), +}) async { + for (int i = 0; i < frames; i++) { + await tester.pump(frameDuration); + } +} + +String _buildDirectionInheritanceHtml({ + required int depth, + required int runCount, +}) { + final String openNodes = + List.filled(depth, '
').join(); + final String closeNodes = List.filled(depth, '
').join(); + final String content = List.generate( + runCount, + (int index) => + 'مرحبا اتجاه ${index + 1} nested text sample', + ).join(); + + return ''' + + + + + + +
$openNodes$content$closeNodes
+ + +'''; +} + +String _buildTextAlignInheritanceHtml({ + required int depth, + required int runCount, +}) { + final String openNodes = + List.filled(depth, '
').join(); + final String closeNodes = List.filled(depth, '
').join(); + final String content = List.generate( + runCount, + (int index) => 'alignment sample ${index + 1}', + ).join(); + + return ''' + + + + + + +
$openNodes$content$closeNodes
+ + +'''; +} + +String _buildParagraphRebuildHtml({ + required int chipCount, +}) { + final String chips = List.generate( + chipCount, + (int index) { + final int tone = index % 4; + final int badge = index % 3; + return ''' + + series ${index + 1} + item ${index + 1} + wrapped inline metrics sample ${index + 1} + +'''; + }, + ).join(); + + return ''' + + + + + + +
+
$chips
+
+ + +'''; +} + +String _buildOpacityTransitionHtml({ + required int tileCount, +}) { + final String tiles = List.generate( + tileCount, + (int index) => + '', + ).join(); + + return ''' + + + + + + +
$tiles
+ + +'''; +} + +String _buildFiatFilterPopupHtml({ + required int optionCount, +}) { + const List currencies = [ + 'USD', + 'EUR', + 'SAR', + 'THB', + 'BRL', + 'JPY', + 'AED', + 'GBP', + ]; + const List names = [ + 'United States Dollar', + 'Euro العربية', + 'Saudi Riyal العربية', + 'Baht ไทย', + 'Real do Brasil', + 'Japanese Yen 日本語', + 'UAE Dirham العربية', + 'Pound Sterling', + ]; + + final String options = List.generate(optionCount, (int index) { + final String currency = currencies[index % currencies.length]; + final String name = names[index % names.length]; + final String badge = index % 3 == 0 ? 'Hot' : (index % 3 == 1 ? 'OTC' : 'New'); + final String iconLabel = currency.substring(0, 1); + return ''' +
+ ${iconLabel} +
+ ${currency} + ${name} ${index + 1} +
+ ${badge} +
+'''; + }).join(); + + return ''' + + + + + + +
+
+
+ F +
+ Fiat currency + USD +
+ +
+ +
+
+ + +'''; +} + +String _buildPaymentMethodSheetHtml({ + required int groupCount, + required int rowsPerGroup, +}) { + const List groupTitles = [ + 'Bank transfer', + 'Digital wallet', + 'Local rails', + 'Express payout', + ]; + const List paymentTitles = [ + 'Bank Express', + 'Wallet Direct', + 'Instant Local', + 'Verified Transfer', + 'Priority Cash', + 'Merchant Route', + ]; + const List subtitles = [ + 'T+0 العربية ไทย', + 'Fast rail 日本語', + 'Local settle عربى', + 'Wide coverage ไทย', + 'Low fee English', + 'High success 日本語', + ]; + + final String groups = List.generate(groupCount, (int groupIndex) { + final String rows = List.generate(rowsPerGroup, (int rowIndex) { + final int absoluteIndex = groupIndex * rowsPerGroup + rowIndex; + final bool extra = rowIndex >= 4; + final String title = paymentTitles[absoluteIndex % paymentTitles.length]; + final String subtitle = subtitles[absoluteIndex % subtitles.length]; + return ''' +
+ P${(absoluteIndex % 7) + 1} + ${absoluteIndex % 3 == 0 ? 'Hot' : 'On'} +
+ ${title} ${(absoluteIndex % 5) + 1} + ${subtitle} ${(absoluteIndex % 8) + 1} +
+ KYC + T+0 + FX +
+
+ ${2 + (absoluteIndex % 6)}m + + +
+'''; + }).join(); + + return ''' +
+
+ ${groupTitles[groupIndex % groupTitles.length]} + ${rowsPerGroup} +
+
$rows
+
+ Show more options + +
+
+'''; + }).join(); + + return ''' + + + + + + +
+
+
+ Select payment method + Confirm +
+
$groups
+
+
+ + +'''; +} + +String _buildPaymentMethodBottomSheetHtml({ + required int groupCount, + required int rowsPerGroup, +}) { + const List groupTitles = [ + 'Bank transfer', + 'Digital wallet', + 'Local rails', + 'Express payout', + ]; + const List paymentTitles = [ + 'Bank Express', + 'Wallet Direct', + 'Instant Local', + 'Verified Transfer', + 'Priority Cash', + 'Merchant Route', + ]; + const List subtitles = [ + 'T+0 العربية ไทย', + 'Fast rail 日本語', + 'Local settle عربى', + 'Wide coverage ไทย', + 'Low fee English', + 'High success 日本語', + ]; + + final String groups = List.generate(groupCount, (int groupIndex) { + final String rows = List.generate(rowsPerGroup, (int rowIndex) { + final int absoluteIndex = groupIndex * rowsPerGroup + rowIndex; + final bool extra = rowIndex >= 4; + final String title = paymentTitles[absoluteIndex % paymentTitles.length]; + final String subtitle = subtitles[absoluteIndex % subtitles.length]; + return ''' +
+ P${(absoluteIndex % 7) + 1} + ${absoluteIndex % 3 == 0 ? 'Hot' : 'On'} +
+ ${title} ${(absoluteIndex % 5) + 1} + ${subtitle} ${(absoluteIndex % 8) + 1} +
+ KYC + T+0 + FX +
+
+ ${2 + (absoluteIndex % 6)}m + + +
+'''; + }).join(); + + return ''' +
+
+ ${groupTitles[groupIndex % groupTitles.length]} + ${rowsPerGroup} +
+
$rows
+
+ Show more options + +
+
+'''; + }).join(); + + return ''' + + + + + + +
+
+
+ +
+ Card + Visa **** 1234 +
+
+ +
+ + +
+
+ Select payment method + Confirm +
+
$groups
+
+
+
+
+ + +'''; +} + +String _buildPaymentMethodFastPathSheetHtml({ + required int groupCount, + required int rowsPerGroup, +}) { + const List groupTitles = [ + 'Bank transfer', + 'Digital wallet', + 'Local rails', + 'Express payout', + ]; + const List paymentTitles = [ + 'Bank Express', + 'Wallet Direct', + 'Instant Local', + 'Verified Transfer', + 'Priority Cash', + 'Merchant Route', + ]; + const List subtitles = [ + 'T+0 العربية ไทย', + 'Fast rail 日本語', + 'Local settle عربى', + 'Wide coverage ไทย', + 'Low fee English', + 'High success 日本語', + ]; + + final String groups = List.generate(groupCount, (int groupIndex) { + final String rows = List.generate(rowsPerGroup, (int rowIndex) { + final int absoluteIndex = groupIndex * rowsPerGroup + rowIndex; + final bool extra = rowIndex >= 5; + final String title = paymentTitles[absoluteIndex % paymentTitles.length]; + final String subtitle = subtitles[absoluteIndex % subtitles.length]; + return ''' +
+
+
${absoluteIndex % 3 == 0 ? 'Hot' : 'On'}
+
+
${title} ${(absoluteIndex % 5) + 1}
+
${subtitle} ${(absoluteIndex % 8) + 1}
+
+
KYC
+
T+0
+
FX
+
+
+
${2 + (absoluteIndex % 6)}m
+
${absoluteIndex % 2 == 0 ? 'AUTO' : 'FAST'}
+
+
+'''; + }).join(); + + return ''' +
+
${groupTitles[groupIndex % groupTitles.length]}
+
$rows
+
Show more options
+
+'''; + }).join(); + + return ''' + + + + + + +
+
+
+
Select payment method
+
Confirm
+
+
$groups
+
+
+ + +'''; +} + +String _buildPaymentMethodBottomSheetTightHtml({ + required int groupCount, + required int rowsPerGroup, +}) { + const List groupTitles = [ + 'Bank transfer', + 'Digital wallet', + 'Local rails', + 'Express payout', + ]; + const List paymentTitles = [ + 'Bank Express', + 'Wallet Direct', + 'Instant Local', + 'Verified Transfer', + 'Priority Cash', + 'Merchant Route', + ]; + const List subtitles = [ + 'T+0 العربية ไทย', + 'Fast rail 日本語', + 'Local settle عربى', + 'Wide coverage ไทย', + 'Low fee English', + 'High success 日本語', + ]; + + final String groups = List.generate(groupCount, (int groupIndex) { + final String rows = List.generate(rowsPerGroup, (int rowIndex) { + final int absoluteIndex = groupIndex * rowsPerGroup + rowIndex; + final bool extra = rowIndex >= 4; + final String title = paymentTitles[absoluteIndex % paymentTitles.length]; + final String subtitle = subtitles[absoluteIndex % subtitles.length]; + return ''' +
+
+
${absoluteIndex % 3 == 0 ? 'Hot' : 'On'}
+
+
${title} ${(absoluteIndex % 5) + 1}
+
${subtitle} ${(absoluteIndex % 8) + 1}
+
+
KYC
+
T+0
+
FX
+
+
+
${2 + (absoluteIndex % 6)}m
+
Route ${(absoluteIndex % 4) + 1}
+
+
+'''; + }).join(); + + return ''' +
+
+
${groupTitles[groupIndex % groupTitles.length]}
+
${rowsPerGroup}
+
+
$rows
+
+
Show more options
+
+
+
+'''; + }).join(); + + return ''' + + + + + + +
+
+
+
+
+
Card
+
Visa **** 1234
+
+
+
+
+ + +
+
+
Select payment method
+
Confirm
+
+
$groups
+
+
+
+
+ + +'''; +} + +String _buildPaymentMethodPickerModalHtml() { + String buildAccounts(String prefix, int count) { + return List.generate(count, (int index) { + final bool selected = index == 0; + final String selectedClass = selected ? ' selected' : ''; + return ''' + +'''; + }).join(); + } + + String buildMethodCard({ + required String id, + required String title, + required String price, + required String description, + required String accounts, + String? tag, + bool selected = false, + bool expanded = false, + }) { + final String selectedClass = selected ? ' selected' : ''; + final String expandedClass = expanded ? ' expanded' : ' collapsed'; + final String tagHtml = tag == null + ? '' + : '$tag'; + return ''' +
+
+
+
+
+
+
$title
+ $tagHtml +
+
$description
+
+
+
+
$price
+
+
+
+ +
+'''; + } + + final String openBankingCard = buildMethodCard( + id: 'open-banking', + title: 'Open Banking', + price: '1.002 EUR', + description: '到账约 1-10 分钟 Fast settle العربية', + accounts: buildAccounts('Barclays', 3), + tag: 'Best Price', + selected: true, + expanded: true, + ); + final String cardPayCard = buildMethodCard( + id: 'card-pay', + title: 'Card', + price: '1.008 EUR', + description: 'Visa / Mastercard ไทย immediate', + accounts: buildAccounts('Visa', 4), + tag: 'Recommended', + ); + final String bankTransferCard = buildMethodCard( + id: 'bank-transfer', + title: 'Bank Transfer', + price: '1.011 EUR', + description: 'SEPA rail payout 日本語 review', + accounts: buildAccounts('SEPA', 4), + ); + final String cvPayCard = buildMethodCard( + id: 'cvpay', + title: 'CVPAY', + price: '1.013 EUR', + description: 'Pending maintenance in 2 hours', + accounts: buildAccounts('VietQR', 2), + ); + + return ''' + + + + + + +
+
+
+
+
+
+
Card
+
Visa **** 1234
+
+
+
+ + +
+
+
+ + + +
+
Bank cards and transfer
+
+ $cardPayCard + $bankTransferCard +
+
$cvPayCard
+
+ Show more + +
+
+
+
+
+ + +'''; +} + +String _buildPaymentMethodOtcSourceSheetHtml({ + required int sectionCount, + required int cardsPerSection, + required int accountsPerCard, +}) { + const List sectionTitles = [ + 'Recommended methods', + 'More payment methods', + 'Popular rails', + ]; + const List cardTitles = [ + 'Open Banking', + 'Bank Transfer', + 'Card', + 'PIX', + 'SEPA', + 'Wallet Pay', + 'Ideal', + ]; + const List cardDescriptions = [ + '到账约 1-10 分钟 العربية', + 'Fast settle ไทย', + 'Low fee 日本語', + 'Pending review عربى', + 'Wide coverage English', + 'Priority rail ไทย', + ]; + const List accountPrefixes = [ + 'Barclays', + 'HSBC', + 'SEPA', + 'PIX', + 'Ideal', + 'Visa', + ]; + + String buildAccountRows(int cardIndex) { + return List.generate(accountsPerCard, (int accountIndex) { + final bool selected = accountIndex == 0 && cardIndex.isEven; + final String selectedClass = selected ? ' selected' : ''; + final String statusText = + accountIndex % 3 == 0 ? 'Ready' : (accountIndex % 3 == 1 ? 'Pending' : 'Saved'); + return ''' + +'''; + }).join(); + } + + String buildCards(int sectionIndex) { + return List.generate(cardsPerSection, (int cardIndex) { + final int absoluteIndex = sectionIndex * cardsPerSection + cardIndex; + final bool extra = cardIndex >= 4; + final bool selected = absoluteIndex == 1; + final bool expanded = cardIndex < 3; + final String extraClass = extra ? ' extra' : ''; + final String selectedClass = selected ? ' selected' : ''; + final String expandedClass = expanded ? ' expanded' : ' collapsed'; + final String title = cardTitles[absoluteIndex % cardTitles.length]; + final String description = + '${cardDescriptions[absoluteIndex % cardDescriptions.length]} ${(absoluteIndex % 4) + 1}'; + final String tag = absoluteIndex % 3 == 0 + ? 'Best Price' + : (absoluteIndex % 3 == 1 ? '0 Fee' : 'Popular'); + return ''' +
+
+
+
+
+
+
+ $title + +
+ $tag +
+
$description
+
+
+
${(1 + ((absoluteIndex % 7) / 1000)).toStringAsFixed(3)} EUR
+
+
+
+ ${buildAccountRows(absoluteIndex)} + +
+
+'''; + }).join(); + } + + final String sections = List.generate(sectionCount, (int sectionIndex) { + final bool expanded = sectionIndex == 0; + return ''' +
+
+
+
${sectionTitles[sectionIndex % sectionTitles.length]}
+
+
+
Grouped quote list with order-based card rendering
+
+
+ ${buildCards(sectionIndex)} +
+ Show more + +
+
+
+'''; + }).join(); + + return ''' + + + + + + +
+
+
$sections
+
+
+ + +'''; +} + +String _buildFlexInlineLayoutHtml({ + required int cardCount, +}) { + final String cards = List.generate(cardCount, (int index) { + final int tone = index % 4; + return ''' +
+
+ series ${index + 1} + issue cluster ${index + 1} + p${(index % 3) + 1} +
+
+ Long wrapped summary for inline layout and flex relayout sample ${index + 1} + active + baseline measurement path ${index + 1} +
+
+ + + active + + +
+
+ ETA ${12 + (index % 9)}h + owner ${index + 3} + series-${(index % 5) + 1} + needs follow-up +
+
+ triage ${index + 1} + queue ${(index % 4) + 1} + hot + w${(index % 6) + 2} +
+
+ sprint ${(index % 3) + 1} + lane ${(index % 5) + 1} + review +
+
+
+ alpha issue ${index + 1} + queue ${(index % 4) + 1} +
+
+ مرحبا فريق ${(index % 7) + 1} + window ${(index % 6) + 2} +
+
+ lane ${(index % 5) + 1} + hot +
+
+ queue ${(index % 4) + 1} follow-up +
+
+
+
+ owner ${(index % 6) + 2} + review +
+
+ ไทย ${(index % 5) + 1} + status +
+
+ p${(index % 3) + 1} + audit +
+
+ follow-up detail ${(index % 8) + 1} +
+
+
+
intl copy ${index + 1}
+
مرحبا ${(index % 7) + 1}
+
ไทย ${(index % 5) + 1}
+
नमस्ते ${(index % 4) + 1}
+
+
+
owner ${(index % 6) + 2}
+
queue ${(index % 4) + 1}
+
series ${(index % 5) + 1}
+
window ${(index % 8) + 3}
+
+
+
alpha ${(index % 9) + 1}
+
beta ${(index % 7) + 1}
+
gamma ${(index % 6) + 1}
+
+
+'''; + }).join(); + + return ''' + + + + + + +
+
$cards
+
+ + +'''; +} + +String _buildFlexAdjustFastPathHtml({ + required int cardCount, +}) { + final String cards = List.generate(cardCount, (int index) { + return ''' +
+
+
p${(index % 3) + 1}
+
lane ${(index % 5) + 1}
+
z${(index % 4) + 1}
+
alpha issue ${index + 1} window ${(index % 6) + 2}
+
queue ${(index % 4) + 1} review ${(index % 3) + 2}
+
ack ${(index % 3) + 1}
+
hot
+
+
+
owner ${(index % 6) + 2}
+
shift ${(index % 3) + 1}
+
مرحبا ${(index % 7) + 1} handoff ${(index % 5) + 1}
+
watch ${(index % 4) + 1} sync ${(index % 3) + 1}
+
ETA ${12 + (index % 8)}h
+
s${(index % 5) + 3}
+
+
+
ไทย ${(index % 5) + 1}
+
k${(index % 4) + 2}
+
follow-up ${(index % 8) + 1} stays pending
+
+ + + +
+
w${(index % 4) + 2}
+
+
+
m${(index % 6) + 1}
+
r${(index % 5) + 2}
+
signal ${(index % 7) + 1} review ${(index % 3) + 1}
+
route ${(index % 4) + 1} hold ${(index % 5) + 1}
+
g${(index % 4) + 3}
+
+
+ + +
go
+
gate ${(index % 4) + 1} check ${(index % 5) + 1}
+
ok
+
+
+ +
handoff ${(index % 4) + 1} queue ${(index % 3) + 1}
+ +
+ + + +
+
trace ${(index % 5) + 2} hold ${(index % 4) + 1}
+
x${(index % 4) + 2}
+
+
+
j${(index % 6) + 1}
+
merge ${(index % 5) + 1} queue ${(index % 4) + 1}
+
watch ${(index % 6) + 1} lane ${(index % 3) + 1}
+ +
+ + + +
+
v${(index % 3) + 3}
+
+
+ +
مرحبا ${(index % 6) + 2} lane ${(index % 3) + 1}
+
lag ${(index % 6) + 1} review ${(index % 4) + 1}
+
+ + +
+
hi
+
+
+
c${(index % 6) + 1}
+
packet ${(index % 5) + 1} queue ${(index % 4) + 2}
+
hold ${(index % 5) + 1} lane ${(index % 3) + 1}
+ +
+ + +
+ +
+
+ +
مرحبا ${(index % 6) + 2} route ${(index % 3) + 1}
+
sync ${(index % 4) + 1} gate ${(index % 5) + 1}
+
ok
+
+ + + +
+
q${(index % 4) + 2}
+
+
+ +
queue ${(index % 6) + 1} watch ${(index % 4) + 1}
+
+ + +
+
gate ${(index % 5) + 1} sync ${(index % 4) + 1}
+ +
r${(index % 4) + 1}
+
+
+ Inline sample ${index + 1} + active + baseline ${index + 1} +
+
+'''; + }).join(); + + return ''' + + + + + + +
+
$cards
+
+ + +'''; } -Future _runFlexInlineLayoutLoop( - _PreparedProfileCase prepared, { - required int mutationIterations, - required List widths, -}) async { - final dom.Element host = prepared.getElementById('host'); - final dom.Element board = prepared.getElementById('board'); - for (int iteration = 0; iteration < mutationIterations; iteration++) { - final int phase = iteration % widths.length; - host.setInlineStyle('width', widths[phase]); - host.setInlineStyle('padding', phase.isEven ? '10px' : '8px'); - board.className = 'phase-$phase'; - board.setInlineStyle('letterSpacing', phase == 1 ? '0.12px' : '0px'); - board.setInlineStyle('wordSpacing', phase == 2 ? '0.35px' : '0px'); - board.style.flushPendingProperties(); - board.ownerDocument.updateStyleIfNeeded(); - await _pumpFrames(prepared.tester, 2); - } +String _buildFlexNestedGroupFastPathHtml({ + required int cardCount, +}) { + final String cards = List.generate(cardCount, (int index) { + return ''' +
+
+
p${(index % 3) + 1}
+
lane ${(index % 5) + 1}
+
+
a${(index % 4) + 1}
+
issue ${index + 1}
+
q${(index % 3) + 1}
+
+
+
b${(index % 4) + 1}
+
queue ${(index % 6) + 2}
+
r${(index % 3) + 2}
+
+
hot
+
+
+
owner ${(index % 6) + 2}
+
shift ${(index % 3) + 1}
+
+
م
+
مرحبا ${(index % 7) + 1}
+
h${(index % 5) + 1}
+
+
+
n${(index % 4) + 1}
+
watch ${(index % 4) + 1}
+
s${(index % 3) + 1}
+
+
ETA ${12 + (index % 8)}h
+
+
+
m${(index % 6) + 1}
+
+
g${(index % 4) + 1}
+
signal ${(index % 7) + 1}
+
x${(index % 3) + 1}
+
+
+
r${(index % 4) + 1}
+
review ${(index % 3) + 1}
+
k${(index % 4) + 1}
+
+
g${(index % 4) + 3}
+
+
+ + +
+
t${(index % 4) + 1}
+
gate ${(index % 4) + 1}
+
c${(index % 3) + 1}
+
+ +
+
+
+
h${(index % 5) + 1}
+
handoff ${(index % 4) + 1}
+
q${(index % 3) + 1}
+
+ +
+
p${(index % 4) + 1}
+
trace ${(index % 5) + 2}
+
w${(index % 4) + 1}
+
+ +
x${(index % 4) + 2}
+
+
+
+
j${(index % 4) + 1}
+
merge ${(index % 5) + 1}
+
v${(index % 3) + 3}
+
+ +
+
z${(index % 4) + 1}
+
lag ${(index % 6) + 1}
+
d${(index % 4) + 2}
+
+ +
+
+ Inline sample ${index + 1} mixed text + active + baseline ${(index % 7) + 1} مرحبا +
+
+'''; + }).join(); + + return ''' + + + + + + +
+
$cards
+
+ + +'''; } -Future _pumpFrames( - WidgetTester tester, - int frames, { - Duration frameDuration = const Duration(milliseconds: 16), -}) async { - for (int i = 0; i < frames; i++) { - await tester.pump(frameDuration); - } +String _buildFlexRunMetricsDenseHtml({ + required int cardCount, +}) { + final String cards = List.generate(cardCount, (int index) { + return ''' +
+
+ p${(index % 3) + 1} + lane ${(index % 5) + 1} + z${(index % 4) + 1} + issue ${index + 1} + w${(index % 6) + 2} queue + a${(index % 3) + 1} + hot +
+
+ owner ${(index % 6) + 2} + s${(index % 3) + 1} + مرحبا ${(index % 7) + 1} + h${(index % 5) + 1} review + ETA ${12 + (index % 8)}h + c${(index % 5) + 3} +
+
+ t${(index % 5) + 1} + k${(index % 4) + 2} + follow ${(index % 8) + 1} +
+ + + +
+ w${(index % 4) + 2} +
+
+ m${(index % 6) + 1} + r${(index % 5) + 2} + signal ${(index % 7) + 1} + r${(index % 3) + 1} hold + g${(index % 4) + 3} +
+
+ + + + gate ${(index % 4) + 1} + k${(index % 4) + 1} +
+
+ + + go + n${(index % 4) + 1} + + + p${(index % 4) + 1} +
+
+ q${(index % 5) + 1} + + ok + + x${(index % 4) + 2} + +
+
+ + hi + + r${(index % 4) + 1} + + m${(index % 5) + 1} + +
+
+ + y${(index % 4) + 2} + lo + + + s${(index % 4) + 1} + +
+
+ Inline sample ${index + 1} mixed text + active + baseline ${(index % 7) + 1} مرحبا +
+
+
+
+ + +
+
+ + +
+
+ p${(index % 4) + 1} +
+
+ + +
+
+ + +
+
+ trace ${(index % 5) + 2} hold +
+
+ h${(index % 5) + 1} + handoff ${(index % 4) + 1} queue ${(index % 3) + 1} +
+
+ + +
+
+ +
+
+ sync ${(index % 4) + 1} + x${(index % 4) + 2} +
+
+ + + lag ${(index % 6) + 1} + + slot ${(index % 4) + 1} + u${(index % 4) + 4} +
+
+ j${(index % 6) + 1} + merge ${(index % 5) + 1} queue ${(index % 4) + 1} + watch ${(index % 6) + 1} + v${(index % 3) + 3} +
+
+ packet ${(index % 4) + 2} queue ${(index % 5) + 1} + hold ${(index % 5) + 1} lane ${(index % 3) + 1} + d${(index % 4) + 2} +
+
+'''; + }).join(); + + return ''' + + + + + + +
+
$cards
+
+ + +'''; } -String _buildDirectionInheritanceHtml({ - required int depth, - required int runCount, +String _buildFlexTightFastPathDenseHtml({ + required int cardCount, }) { - final String openNodes = - List.filled(depth, '
').join(); - final String closeNodes = List.filled(depth, '
').join(); - final String content = List.generate( - runCount, - (int index) => - 'مرحبا اتجاه ${index + 1} nested text sample', - ).join(); + final String cards = List.generate(cardCount, (int index) { + return ''' +
+
+ p${(index % 3) + 1} + lane ${(index % 5) + 1} + issue ${index + 1} queue ${(index % 6) + 2} مرحبا ${(index % 4) + 1} + window ${(index % 6) + 2} alert ${(index % 5) + 1} review + hot +
+
+ owner ${(index % 6) + 2} + + مرحبا ${(index % 7) + 1} handoff ${(index % 5) + 1} shift + + ETA ${12 + (index % 8)}h +
+
+ m${(index % 6) + 1} + signal ${(index % 7) + 1} review ${(index % 3) + 1} + r${(index % 3) + 1} hold queue ${(index % 4) + 1} + g${(index % 4) + 3} +
+
+ + + go + gate ${(index % 4) + 1} check ${(index % 5) + 1} + k${(index % 4) + 1} +
+
+
+ + +
+ p${(index % 4) + 1} +
+ + +
+ trace ${(index % 5) + 2} hold ${(index % 3) + 1} +
+
+ h${(index % 5) + 1} + handoff ${(index % 4) + 1} queue ${(index % 3) + 1} +
+ + +
+ sync ${(index % 4) + 1} lane ${(index % 3) + 1} + x${(index % 4) + 2} +
+
+ Inline sample ${index + 1} mixed text + active + baseline ${(index % 7) + 1} مرحبا +
+
+ j${(index % 6) + 1} + merge ${(index % 5) + 1} queue ${(index % 4) + 1} + watch ${(index % 6) + 1} hold + v${(index % 3) + 3} +
+
+ + + lag ${(index % 6) + 1} review ${(index % 4) + 1} + ok + slot ${(index % 4) + 1} route ${(index % 3) + 2} + u${(index % 4) + 4} +
+
+ + +
+ + +
+ go + + +
+ + +
+
+
+ +
+ + +
+ + ok +
+ + +
+ + +
+
+ + queue ${(index % 6) + 1} watch ${(index % 4) + 1} + gate ${(index % 5) + 1} sync + + q${(index % 4) + 1} +
+
+ c${(index % 5) + 1} + مرحبا ${(index % 6) + 2} lane ${(index % 3) + 1} +
+ + +
+ hold ${(index % 5) + 2} trace + +
+
+ + packet ${(index % 5) + 1} queue ${(index % 4) + 2} + go + lag ${(index % 6) + 1} review + z${(index % 4) + 2} +
+
+'''; + }).join(); return ''' @@ -485,92 +6739,379 @@ String _buildDirectionInheritanceHtml({ - - -
$openNodes$content$closeNodes
- - -'''; -} - -String _buildTextAlignInheritanceHtml({ - required int depth, - required int runCount, -}) { - final String openNodes = - List.filled(depth, '
').join(); - final String closeNodes = List.filled(depth, '
').join(); - final String content = List.generate( - runCount, - (int index) => 'alignment sample ${index + 1}', - ).join(); - - return ''' - - - - -
$openNodes$content$closeNodes
+
+
$cards
+
'''; } -String _buildParagraphRebuildHtml({ - required int chipCount, +String _buildFlexHybridFastPathDenseHtml({ + required int cardCount, }) { - final String chips = List.generate( - chipCount, - (int index) { - final int tone = index % 4; - final int badge = index % 3; - return ''' - - series ${index + 1} - item ${index + 1} - wrapped inline metrics sample ${index + 1} - + final String cards = List.generate(cardCount, (int index) { + return ''' +
+
+ p${(index % 3) + 1} + lane ${(index % 5) + 1} + z${(index % 4) + 1} + issue ${index + 1} + w${(index % 6) + 2} queue + a${(index % 3) + 1} + hot +
+
+ owner ${(index % 6) + 2} + s${(index % 3) + 1} + مرحبا ${(index % 7) + 1} + h${(index % 5) + 1} review + ETA ${12 + (index % 8)}h + c${(index % 5) + 3} +
+
+ t${(index % 5) + 1} + k${(index % 4) + 2} + follow ${(index % 8) + 1} +
+ + + +
+ w${(index % 4) + 2} +
+
+ m${(index % 6) + 1} + r${(index % 5) + 2} + signal ${(index % 7) + 1} + r${(index % 3) + 1} hold + g${(index % 4) + 3} +
+
+ + + go + gate ${(index % 4) + 1} + k${(index % 4) + 1} +
+
+ + queue ${(index % 6) + 1} + + hold ${(index % 5) + 1} + ok +
+
+ Inline sample ${index + 1} mixed text + active + baseline ${(index % 7) + 1} مرحبا +
+
+
+ + + +
+ p${(index % 4) + 1} +
+ + + +
+ trace ${(index % 5) + 2} hold +
+
+ h${(index % 5) + 1} + مرحبا ${(index % 6) + 1} + +
+ + +
+ sync ${(index % 4) + 1} + +
+
+ h${(index % 5) + 1} + handoff ${(index % 4) + 1} queue ${(index % 3) + 1} +
+ + + +
+ sync ${(index % 4) + 1} + x${(index % 4) + 2} +
+
+ + + lag ${(index % 6) + 1} + ok + slot ${(index % 4) + 1} + u${(index % 4) + 4} +
+
+ + packet ${(index % 5) + 1} +
+ + +
+ watch ${(index % 6) + 1} + + go +
+
+ + +
+ + +
+ g${(index % 4) + 1} + +
+ + +
+ +
+
+ +
+ + +
+ + h${(index % 3) + 1} +
+ + +
+ + +
+
+ j${(index % 6) + 1} + merge ${(index % 5) + 1} queue ${(index % 4) + 1} + watch ${(index % 6) + 1} + v${(index % 3) + 3} +
+
+ packet ${(index % 4) + 2} queue ${(index % 5) + 1} + hold ${(index % 5) + 1} lane ${(index % 3) + 1} + d${(index % 4) + 2} +
+
'''; - }, - ).join(); + }).join(); return ''' @@ -579,197 +7120,306 @@ String _buildParagraphRebuildHtml({ - - -
-
$chips
-
- - -'''; -} - -String _buildOpacityTransitionHtml({ - required int tileCount, -}) { - final String tiles = List.generate( - tileCount, - (int index) => - '', - ).join(); - - return ''' - - - - -
$tiles
+
+
$cards
+
'''; } -String _buildFlexInlineLayoutHtml({ +String _buildFlexAdjustWidgetDenseHtml({ required int cardCount, }) { final String cards = List.generate(cardCount, (int index) { - final int tone = index % 4; return ''' -
-
- -
- issue cluster ${index + 1} -
-
- p${(index % 3) + 1} -
+
+
+ p${(index % 3) + 1} + lane ${(index % 5) + 1} + z${(index % 4) + 1} + issue ${index + 1} window ${(index % 6) + 2} + a${(index % 3) + 1} + hot
-
- Long wrapped summary for inline layout and flex relayout sample ${index + 1} - active - baseline measurement path ${index + 1} +
+ owner ${(index % 6) + 2} + s${(index % 3) + 1} + مرحبا ${(index % 7) + 1} handoff ${(index % 5) + 1} + review ${(index % 5) + 1} + ETA ${12 + (index % 8)}h +
+
+ ไทย ${(index % 5) + 1} + k${(index % 4) + 2} + follow ${(index % 8) + 1} stays auto + w${(index % 4) + 2} +
+
+ m${(index % 6) + 1} + r${(index % 5) + 2} + signal ${(index % 7) + 1} review ${(index % 3) + 1} + g${(index % 4) + 3}
-
- -
-
- -
-
- active -
-
- -
+ + + + k${(index % 4) + 1}
-
-
- ETA ${12 + (index % 9)}h -
-
- owner ${index + 3} -
-
- series-${(index % 5) + 1} -
-
- needs follow-up -
+
+ + guard ${(index % 6) + 1} + + t${(index % 5) + 2} +
+
+ Inline sample ${index + 1} mixed text + active + baseline ${(index % 7) + 1} مرحبا +
+
+ h${(index % 5) + 1} + handoff ${(index % 4) + 1} ledger ${(index % 5) + 1} + lag ${(index % 6) + 1} + x${(index % 4) + 2}
'''; @@ -786,7 +7436,7 @@ String _buildFlexInlineLayoutHtml({ background: #ffffff; } #host { - width: 360px; + width: 368px; padding: 10px; border: 1px solid #d8dde6; box-sizing: border-box; @@ -801,102 +7451,116 @@ String _buildFlexInlineLayoutHtml({ .card { display: block; flex: 0 1 auto; - width: 156px; min-width: 140px; + max-width: 172px; padding: 8px 10px; border: 1px solid rgba(148, 163, 184, 0.45); border-radius: 12px; color: #0f172a; - text-decoration: none; background: rgba(248, 250, 252, 0.96); box-sizing: border-box; } - .headline, - .controls, - .summary, - .meta { - display: block; - } - .headline, + .queue, + .owners, + .notes, + .signals, .controls, - .meta { + .handoff { display: flex; flex-wrap: nowrap; align-items: flex-start; gap: 4px; - } - .headline { - margin-bottom: 6px; + margin-top: 6px; line-height: 1.35; + color: #334155; } - .summary { - text-indent: 6px; - line-height: 1.55; + .queue { + margin-top: 0; } - .controls { + .summary { margin-top: 6px; - line-height: 1.4; + text-indent: 4px; + line-height: 1.55; } - .meta { - margin-top: 6px; - color: #475569; - line-height: 1.4; + .fixed { + flex: 0 0 auto; } - .slot { - display: block; + .auto-copy, + .auto-note { flex: 0 1 auto; + min-width: 0; + display: block; + color: #1e293b; } - .eyebrow-slot { - width: 56px; + .summary-copy { + color: #1e293b; } - .title-slot { - width: 72px; + .auto-copy.emphasis, + .summary-copy.emphasis { + font-style: italic; } - .priority-slot { - width: 42px; + .tool-field { + font: inherit; + line-height: 1.2; + box-sizing: border-box; + } + .tool-input-a { + width: 72px; } - .picker-slot { + .tool-input-b { width: 58px; } - .note-slot { - width: 48px; + .tool-select-a { + width: 66px; } - .status-slot { - width: 52px; + .tool-select-b { + width: 58px; } - .flag-slot { - width: 18px; + .tool-chip { + width: 32px; + text-align: center; + box-sizing: border-box; } - .eta-slot { - width: 40px; + .severity { + width: 34px; + text-align: center; + box-sizing: border-box; } - .owner-slot { - width: 48px; + .lane { + width: 44px; } - .soft-slot { - width: 60px; + .zone { + width: 30px; } - .trailing-slot { - width: 74px; + .ack { + width: 36px; } - .eyebrow { - color: #64748b; - text-decoration: none; + .badge { + width: 36px; + text-align: center; + box-sizing: border-box; } - .title { - font-weight: 700; + .owner { + width: 50px; } - .copy { - color: #1e293b; + .shift { + width: 42px; } - .copy.emphasis { - font-style: italic; + .eta { + width: 42px; } - .metric.trailing { - font-style: italic; + .marker { + width: 28px; + text-align: center; + box-sizing: border-box; + } + .chip.subtle { + width: 34px; + text-align: center; + box-sizing: border-box; } .chip { - display: block; + display: inline-block; padding: 1px 7px; border-radius: 999px; border: 1px solid rgba(59, 130, 246, 0.24); @@ -905,84 +7569,6 @@ String _buildFlexInlineLayoutHtml({ line-height: 18px; vertical-align: middle; } - .picker, - .note { - vertical-align: middle; - font: inherit; - line-height: 18px; - } - .picker { - width: 58px; - height: 22px; - } - .note { - width: 48px; - height: 22px; - padding: 0 4px; - box-sizing: border-box; - } - .flag { - display: block; - margin: 0 auto; - vertical-align: middle; - } - #board.phase-1 .card { - width: 148px; - } - #board.phase-1 .headline { - font-style: italic; - } - #board.phase-1 .summary { - text-indent: 10px; - line-height: 1.62; - } - #board.phase-1 .title-slot { - width: 68px; - } - #board.phase-1 .trailing-slot { - width: 68px; - } - #board.phase-2 .card { - width: 168px; - } - #board.phase-2 .chip { - padding: 2px 9px; - font-size: 13px; - } - #board.phase-2 .status-slot { - width: 58px; - } - #board.phase-2 .soft-slot { - width: 66px; - } - #board.phase-2 .copy.emphasis { - font-weight: 700; - } - #board.phase-3 .summary { - word-break: break-word; - line-height: 1.7; - } - #board.phase-3 .meta { - letter-spacing: 0.2px; - } - #board.phase-3 .headline { - gap: 3px; - } - #board.phase-3 .controls { - gap: 3px; - } - .tone0 .priority { - background: rgba(253, 230, 138, 0.55); - } - .tone1 .priority { - background: rgba(187, 247, 208, 0.55); - } - .tone2 .priority { - background: rgba(216, 180, 254, 0.45); - } - .tone3 .priority { - background: rgba(254, 205, 211, 0.5); - } diff --git a/integration_tests/lib/custom_elements/flutter_bottom_sheet.dart b/integration_tests/lib/custom_elements/flutter_bottom_sheet.dart new file mode 100644 index 0000000000..8fd12bb77e --- /dev/null +++ b/integration_tests/lib/custom_elements/flutter_bottom_sheet.dart @@ -0,0 +1,211 @@ +import 'package:flutter/material.dart'; +import 'package:webf/rendering.dart'; +import 'package:webf/webf.dart'; + +class FlutterBottomSheetElement extends WidgetElement { + FlutterBottomSheetElement(super.context); + + @override + FlutterBottomSheetElementState? get state => + super.state as FlutterBottomSheetElementState?; + + static StaticDefinedAsyncBindingObjectMethodMap customAsyncMethods = { + 'open': StaticDefinedAsyncBindingObjectMethod( + call: (element, args) async { + return castToType(element) + .state + ?.showBottomSheet(); + }, + ), + 'close': StaticDefinedAsyncBindingObjectMethod( + call: (element, args) async { + return castToType(element) + .state + ?.closeBottomSheet(); + }, + ), + }; + + @override + List get asyncMethods => [ + ...super.asyncMethods, + FlutterBottomSheetElement.customAsyncMethods, + ]; + + @override + WebFWidgetElementState createState() => FlutterBottomSheetElementState(this); +} + +class FlutterBottomSheetElementState extends WebFWidgetElementState { + FlutterBottomSheetElementState(super.widgetElement); + + NavigatorState? _navigator; + bool _closeFromAttribute = false; + + @override + FlutterBottomSheetElement get widgetElement => + super.widgetElement as FlutterBottomSheetElement; + + Future showBottomSheet() async { + if (!mounted) return; + + final String title = widgetElement.getAttribute('title') ?? ''; + final String primaryBtnTitle = + widgetElement.getAttribute('primary-btn-title') ?? ''; + final bool isDismissible = + _parseBool(widgetElement.getAttribute('is-dismissible')) ?? true; + final bool enableDrag = + _parseBool(widgetElement.getAttribute('enable-drag')) ?? true; + + final Iterable contents = + widgetElement.childNodes.whereType(); + assert( + contents.isNotEmpty && contents.length == 1, + 'flutter-bottom-sheet expects exactly one flutter-popup-item child.', + ); + + final Widget contentWidget = + WebFWidgetElementChild(child: contents.first.toWidget()); + + _closeFromAttribute = false; + try { + await showModalBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: isDismissible, + enableDrag: enableDrag, + useSafeArea: true, + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.85, + minHeight: 176 + MediaQuery.of(context).padding.bottom, + ), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (BuildContext modalContext) { + _navigator = Navigator.of(modalContext); + return Padding( + padding: EdgeInsets.fromLTRB( + 16, + 0, + 16, + MediaQuery.of(modalContext).viewInsets.bottom + 16, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 8), + Container( + width: 36, + height: 4, + decoration: BoxDecoration( + color: Colors.black26, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(height: 12), + if (title.isNotEmpty) ...[ + const SizedBox(height: 4), + SizedBox( + height: 26, + child: Center( + child: Text( + title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + const SizedBox(height: 16), + ] else + const SizedBox(height: 8), + Flexible( + child: Scrollbar( + child: SingleChildScrollView( + child: contentWidget, + ), + ), + ), + if (primaryBtnTitle.isNotEmpty) const SizedBox(height: 16), + if (primaryBtnTitle.isNotEmpty) + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + widgetElement.dispatchEvent(Event('onconfirm')); + }, + child: Text(primaryBtnTitle), + ), + ), + const SizedBox(height: 8), + ], + ), + ); + }, + ); + } finally { + _navigator = null; + } + + if (!_closeFromAttribute) { + widgetElement.dispatchEvent(Event('onmask')); + } + _closeFromAttribute = false; + } + + void closeBottomSheet() { + _closeFromAttribute = true; + _navigator?.pop(); + } + + @override + Widget build(BuildContext context) => const SizedBox.shrink(); + + bool? _parseBool(String? value) { + if (value == null || value.isEmpty) return null; + if (value == 'true' || value == '1') return true; + if (value == 'false' || value == '0') return false; + return null; + } +} + +class FlutterPopupItemElement extends WidgetElement { + FlutterPopupItemElement(super.context); + + @override + Map get defaultStyle => const { + 'display': 'block', + }; + + @override + WebFWidgetElementState createState() => FlutterPopupItemElementState(this); +} + +class FlutterPopupItemElementState extends WebFWidgetElementState { + FlutterPopupItemElementState(super.widgetElement); + + @override + FlutterPopupItemElement get widgetElement => + super.widgetElement as FlutterPopupItemElement; + + @override + Widget build(BuildContext context) { + if (widgetElement.childNodes.length == 1) { + return WebFWidgetElementChild( + child: widgetElement.childNodes.first.toWidget(), + ); + } + + return WebFWidgetElementChild( + child: WebFHTMLElement( + tagName: 'DIV', + controller: widgetElement.controller, + parentElement: widgetElement, + children: widgetElement.childNodes.toWidgetList(), + ), + ); + } +} diff --git a/integration_tests/lib/custom_elements/main.dart b/integration_tests/lib/custom_elements/main.dart index ec2e24607e..d2a43470fc 100644 --- a/integration_tests/lib/custom_elements/main.dart +++ b/integration_tests/lib/custom_elements/main.dart @@ -24,6 +24,7 @@ import 'flutter_max_height_container.dart'; import 'flutter_fixed_height_slot.dart'; import 'flutter_cupertino_portal_modal_popup.dart'; import 'flutter_portal_popup_item.dart'; +import 'flutter_bottom_sheet.dart'; import 'flutter_ifc_host.dart'; void defineWebFCustomElements() { @@ -56,6 +57,8 @@ void defineWebFCustomElements() { WebF.defineCustomElement( 'flutter-cupertino-portal-modal-popup', (context) => FlutterCupertinoPortalModalPopup(context)); WebF.defineCustomElement('flutter-portal-popup-item', (context) => FlutterPortalPopupItem(context)); + WebF.defineCustomElement('flutter-bottom-sheet', (context) => FlutterBottomSheetElement(context)); + WebF.defineCustomElement('flutter-popup-item', (context) => FlutterPopupItemElement(context)); WebF.defineCustomElement('flutter-intrinsic-container', (context) => FlutterIntrinsicContainer(context)); WebF.defineCustomElement('sample-container', (context) => SampleContainer(context)); WebF.defineCustomElement('native-flex', (context) => NativeFlexContainer(context)); diff --git a/integration_tests/scripts/profile_hotspots_integration.js b/integration_tests/scripts/profile_hotspots_integration.js index 1e66daf1b5..add9efc846 100644 --- a/integration_tests/scripts/profile_hotspots_integration.js +++ b/integration_tests/scripts/profile_hotspots_integration.js @@ -1,44 +1,189 @@ +const fs = require('fs'); +const fsp = require('fs/promises'); const {spawn} = require('child_process'); const os = require('os'); const path = require('path'); -function getDeviceName() { +const PROFILE_CASE_FILTER_DEFINE = '--dart-define=WEBF_PROFILE_CASE_FILTER='; +const WORKING_DIRECTORY = path.join(__dirname, '..'); +const PROFILE_CASES_TEST_PATH = path.join( + WORKING_DIRECTORY, + 'integration_test', + 'profile_hotspot_cases_test.dart', +); +const PROFILE_OUTPUT_DIRECTORY = path.join( + WORKING_DIRECTORY, + 'build', + 'profile_hotspots', +); + +function getDefaultDeviceName() { if (os.platform() === 'darwin') return 'macos'; if (os.platform() === 'linux') return 'linux'; if (os.platform() === 'win32') return 'windows'; throw new Error(`Unsupported platform: ${os.platform()}`); } -function main() { - const deviceName = getDeviceName(); - const workingDirectory = path.join(__dirname, '..'); - const args = [ +function resolveDevice(argv) { + const deviceArg = argv.find((arg) => arg.startsWith('--device=')); + if (deviceArg) { + return { + deviceName: deviceArg.slice('--device='.length), + remainingArgs: argv.filter((arg) => arg !== deviceArg), + }; + } + + if (process.env.WEBF_PROFILE_DEVICE) { + return { + deviceName: process.env.WEBF_PROFILE_DEVICE, + remainingArgs: argv, + }; + } + + return { + deviceName: getDefaultDeviceName(), + remainingArgs: argv, + }; +} + +function shouldForceNoDds(deviceName) { + return !['macos', 'linux', 'windows'].includes(deviceName); +} + +function isMobileDevice(deviceName) { + return shouldForceNoDds(deviceName); +} + +function extractRequestedCaseIds(argv) { + const filterArg = argv.find((arg) => arg.startsWith(PROFILE_CASE_FILTER_DEFINE)); + if (!filterArg) { + return { + requestedCaseIds: null, + remainingArgs: argv, + }; + } + + return { + requestedCaseIds: filterArg + .slice(PROFILE_CASE_FILTER_DEFINE.length) + .split(',') + .map((value) => value.trim()) + .filter(Boolean), + remainingArgs: argv.filter((arg) => arg !== filterArg), + }; +} + +function loadProfileCaseIds() { + const source = fs.readFileSync(PROFILE_CASES_TEST_PATH, 'utf8'); + return [...source.matchAll(/testWidgets\('([^']+)'/g)].map((match) => match[1]); +} + +function buildFlutterDriveArgs(deviceName, extraArgs) { + const hasDdsFlag = extraArgs.includes('--no-dds') || extraArgs.includes('--dds'); + return [ 'drive', '-d', deviceName, '--driver=test_driver/profile_hotspot_cases_test.dart', '--target=integration_test/profile_hotspot_cases_test.dart', - ...process.argv.slice(2), + ...(shouldForceNoDds(deviceName) && !hasDdsFlag ? ['--no-dds'] : []), + ...extraArgs, ]; +} - const child = spawn('flutter', args, { - cwd: workingDirectory, - env: { - ...process.env, - NO_PROXY: process.env.NO_PROXY || '127.0.0.1,localhost', - no_proxy: process.env.no_proxy || '127.0.0.1,localhost', - }, - stdio: 'inherit', - }); +function runFlutterDrive(args) { + return new Promise((resolve, reject) => { + const child = spawn('flutter', args, { + cwd: WORKING_DIRECTORY, + env: { + ...process.env, + NO_PROXY: process.env.NO_PROXY || '127.0.0.1,localhost', + no_proxy: process.env.no_proxy || '127.0.0.1,localhost', + }, + stdio: 'inherit', + }); - child.on('close', (code) => { - process.exit(code ?? 1); - }); + child.on('close', (code) => { + if ((code ?? 1) === 0) { + resolve(); + return; + } + reject(new Error(`flutter drive exited with code ${code ?? 1}`)); + }); - child.on('error', (error) => { - console.error('profile hotspot integration failed', error); - process.exit(1); + child.on('error', reject); }); } -main(); +async function readJson(filePath) { + return JSON.parse(await fsp.readFile(filePath, 'utf8')); +} + +async function writeJson(filePath, data) { + await fsp.writeFile(filePath, `${JSON.stringify(data, null, 2)}\n`); +} + +async function resetProfileOutputDirectory() { + await fsp.rm(PROFILE_OUTPUT_DIRECTORY, {recursive: true, force: true}); + await fsp.mkdir(PROFILE_OUTPUT_DIRECTORY, {recursive: true}); +} + +async function mergeCaseArtifacts(aggregateData) { + const caseManifest = await readJson(path.join(PROFILE_OUTPUT_DIRECTORY, 'manifest.json')); + Object.assign(aggregateData, caseManifest); +} + +async function writeAggregateArtifacts(aggregateData) { + await writeJson(path.join(PROFILE_OUTPUT_DIRECTORY, 'all_cases.json'), aggregateData); + await writeJson(path.join(PROFILE_OUTPUT_DIRECTORY, 'manifest.json'), aggregateData); +} + +async function runMobileCasesIndividually({ + deviceName, + baseArgs, + caseIds, +}) { + const aggregateData = {}; + await resetProfileOutputDirectory(); + + for (const caseId of caseIds) { + console.log(`\n=== Running Android profile case: ${caseId} ===`); + await runFlutterDrive( + buildFlutterDriveArgs(deviceName, [ + ...baseArgs, + `${PROFILE_CASE_FILTER_DEFINE}${caseId}`, + ]), + ); + await mergeCaseArtifacts(aggregateData); + } + + await writeAggregateArtifacts(aggregateData); +} + +async function main() { + const {deviceName, remainingArgs: deviceArgs} = resolveDevice(process.argv.slice(2)); + const {requestedCaseIds, remainingArgs} = extractRequestedCaseIds(deviceArgs); + const requestedOrAllCaseIds = requestedCaseIds ?? loadProfileCaseIds(); + + if (isMobileDevice(deviceName)) { + await runMobileCasesIndividually({ + deviceName, + baseArgs: remainingArgs, + caseIds: requestedOrAllCaseIds, + }); + return; + } + + const args = buildFlutterDriveArgs(deviceName, [ + ...remainingArgs, + ...(requestedCaseIds + ? [`${PROFILE_CASE_FILTER_DEFINE}${requestedCaseIds.join(',')}`] + : []), + ]); + await runFlutterDrive(args); +} + +main().catch((error) => { + console.error('profile hotspot integration failed', error); + process.exit(1); +}); diff --git a/integration_tests/test_driver/profile_hotspot_cases_test.dart b/integration_tests/test_driver/profile_hotspot_cases_test.dart index ef4b794b7c..ef84ed76f4 100644 --- a/integration_tests/test_driver/profile_hotspot_cases_test.dart +++ b/integration_tests/test_driver/profile_hotspot_cases_test.dart @@ -1,52 +1,156 @@ import 'dart:convert'; import 'dart:io'; -import 'package:integration_test/integration_test_driver.dart'; +import 'package:flutter_driver/flutter_driver.dart'; import 'package:path/path.dart' as path; Future main() async { - await integrationDriver( - responseDataCallback: (Map? data) async { - if (data == null) { - return; - } + final FlutterDriver driver = await FlutterDriver.connect(); + final int startMicros = + (await driver.serviceClient.getVMTimelineMicros()).timestamp!; - final Directory outputDirectory = Directory( - path.join(Directory.current.path, 'build', 'profile_hotspots'), - )..createSync(recursive: true); + late final Map response; + try { + final String jsonResult = await driver.requestData( + null, + timeout: const Duration(minutes: 20), + ); + response = (json.decode(jsonResult) as Map) + .cast(); + } finally { + // Keep the VM service connection alive until after any host-side CPU + // samples are collected below. + } - final Map response = Map.from(data); - final Map manifest = {}; + final int endMicros = + (await driver.serviceClient.getVMTimelineMicros()).timestamp!; + final Map? responseData = + (response['data'] as Map?)?.cast(); - await _writeJson( - outputDirectory, - response, - testOutputFilename: 'all_cases', - ); + if (responseData != null) { + await _materializeDriverCpuSamples( + driver, + responseData, + startMicros: startMicros, + endMicros: endMicros, + ); + await _writeResponseData(responseData); + } + + await driver.close(); + + final bool allTestsPassed = response['result'] == 'true'; + if (allTestsPassed) { + stdout.writeln('All tests passed.'); + exit(0); + } + + final List failureDetails = + (response['failureDetails'] as List? ?? []); + if (failureDetails.isNotEmpty) { + stdout.writeln('Failure Details:'); + for (final dynamic failure in failureDetails) { + stdout.writeln(json.decode(failure as String)['details']); + } + } + exit(1); +} + +Future _materializeDriverCpuSamples( + FlutterDriver driver, + Map responseData, { + required int startMicros, + required int endMicros, +}) async { + final List>> pendingCpuCaptures = + responseData.entries + .where((MapEntry entry) { + return entry.key.endsWith('_cpu_samples') && + entry.value is Map && + (entry.value as Map)['captureMode'] == + 'driver'; + }) + .map((MapEntry entry) => MapEntry>( + entry.key, + Map.from(entry.value as Map), + )) + .toList(); + + if (pendingCpuCaptures.isEmpty) { + return; + } - for (final MapEntry entry in response.entries) { - if ((entry.key.endsWith('_timeline') || - entry.key.endsWith('_cpu_samples')) && - entry.value is Map) { - await _writeJson( - outputDirectory, - Map.from(entry.value as Map), - testOutputFilename: entry.key, - ); - manifest[entry.key] = { - 'path': path.join(outputDirectory.path, '${entry.key}.json'), - }; - } else { - manifest[entry.key] = entry.value; - } - } + final int timeExtentMicros = + endMicros > startMicros ? endMicros - startMicros : 1; + final Map allSamples = (await driver.serviceClient + .getCpuSamples(driver.appIsolate.id!, startMicros, timeExtentMicros)) + .toJson(); + final List allSampleEntries = + (allSamples['samples'] as List? ?? []); + for (final MapEntry> pendingCpuCapture + in pendingCpuCaptures) { + final String profileLabel = + pendingCpuCapture.value['profileLabel'] as String? ?? ''; + final List filteredSamples = allSampleEntries + .where((dynamic sample) => + sample is Map && + sample['userTag'] == profileLabel) + .toList(); + + final Map filteredCpuSamples = + Map.from(allSamples); + filteredCpuSamples['sampleCount'] = filteredSamples.length; + filteredCpuSamples['timeOriginMicros'] = startMicros; + filteredCpuSamples['timeExtentMicros'] = timeExtentMicros; + filteredCpuSamples['samples'] = filteredSamples; + + responseData[pendingCpuCapture.key] = { + 'profileLabel': profileLabel, + 'isolateId': driver.appIsolate.id, + 'timeOriginMicros': startMicros, + 'timeExtentMicros': timeExtentMicros, + 'samples': filteredCpuSamples, + }; + } +} + +Future _writeResponseData(Map data) async { + final Directory outputDirectory = Directory( + path.join(Directory.current.path, 'build', 'profile_hotspots'), + )..createSync(recursive: true); + + final Map response = Map.from(data); + final Map manifest = {}; + + await _writeJson( + outputDirectory, + response, + testOutputFilename: 'all_cases', + ); + + for (final MapEntry entry in response.entries) { + if ((entry.key.endsWith('_timeline') || + entry.key.endsWith('_cpu_samples')) && + entry.value is Map) { await _writeJson( outputDirectory, - manifest, - testOutputFilename: 'manifest', + Map.from(entry.value as Map), + testOutputFilename: entry.key, ); - }, + manifest[entry.key] = { + 'path': path.join(outputDirectory.path, '${entry.key}.json'), + }; + } else { + manifest[entry.key] = entry.value; + } + } + + await _writeJson( + outputDirectory, + manifest, + testOutputFilename: 'manifest', ); } diff --git a/webf/android/build.gradle b/webf/android/build.gradle index 621a667978..67fe11bded 100644 --- a/webf/android/build.gradle +++ b/webf/android/build.gradle @@ -25,6 +25,12 @@ apply plugin: 'com.android.library' def enableTestBridge = System.getenv('WEBF_ENABLE_TEST') == 'true' || System.getenv('KRAKEN_ENABLE_TEST') == 'true' +def webfTargetAbis = + ((project.findProperty('webfTargetAbis') ?: 'arm64-v8a') + .toString() + .split(',')) + .collect { it.trim() } + .findAll { !it.isEmpty() } android { @@ -42,6 +48,9 @@ android { defaultConfig { minSdk = 21 + ndk { + abiFilters.addAll(webfTargetAbis) + } externalNativeBuild { cmake { arguments "-DANDROID_STL=c++_shared", "-DIS_ANDROID=TRUE", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON" From ec95a67c4e55c64cdc6073c25c865bb4518561d3 Mon Sep 17 00:00:00 2001 From: andycall Date: Thu, 26 Mar 2026 20:19:07 -0700 Subject: [PATCH 03/13] perf(webf): reduce relayouts in flex fast path --- webf/lib/src/foundation/debug_flags.dart | 40 +- webf/lib/src/rendering/flex.dart | 2044 +++++++++++++++------- 2 files changed, 1461 insertions(+), 623 deletions(-) diff --git a/webf/lib/src/foundation/debug_flags.dart b/webf/lib/src/foundation/debug_flags.dart index 7857ce2c15..43fa3cff67 100644 --- a/webf/lib/src/foundation/debug_flags.dart +++ b/webf/lib/src/foundation/debug_flags.dart @@ -117,20 +117,38 @@ class DebugFlags { static int cssGridProfilingMinMs = 2; // Removed: Use FlexLog filters to enable flex logs. - static bool enableFlexFastPathProfiling = - const bool.fromEnvironment('WEBF_DEBUG_FLEX_FAST_PATH', defaultValue: false); - static int flexFastPathProfilingSummaryEvery = - const int.fromEnvironment('WEBF_DEBUG_FLEX_FAST_PATH_SUMMARY_EVERY', defaultValue: 50); - static int flexFastPathProfilingMaxDetailLogs = - const int.fromEnvironment('WEBF_DEBUG_FLEX_FAST_PATH_MAX_DETAIL_LOGS', defaultValue: 20); - static bool enableFlexAnonymousMetricsProfiling = - const bool.fromEnvironment('WEBF_DEBUG_FLEX_ANON_METRICS', defaultValue: false); + static bool enableFlexFastPathProfiling = const bool.fromEnvironment( + 'WEBF_DEBUG_FLEX_FAST_PATH', + defaultValue: false); + static int flexFastPathProfilingSummaryEvery = const int.fromEnvironment( + 'WEBF_DEBUG_FLEX_FAST_PATH_SUMMARY_EVERY', + defaultValue: 50); + static int flexFastPathProfilingMaxDetailLogs = const int.fromEnvironment( + 'WEBF_DEBUG_FLEX_FAST_PATH_MAX_DETAIL_LOGS', + defaultValue: 20); + static bool enableFlexAdjustFastPathProfiling = const bool.fromEnvironment( + 'WEBF_DEBUG_FLEX_ADJUST_FAST_PATH', + defaultValue: false); + static int flexAdjustFastPathProfilingSummaryEvery = + const int.fromEnvironment( + 'WEBF_DEBUG_FLEX_ADJUST_FAST_PATH_SUMMARY_EVERY', + defaultValue: 50); + static int flexAdjustFastPathProfilingMaxDetailLogs = + const int.fromEnvironment( + 'WEBF_DEBUG_FLEX_ADJUST_FAST_PATH_MAX_DETAIL_LOGS', + defaultValue: 20); + static bool enableFlexAnonymousMetricsProfiling = const bool.fromEnvironment( + 'WEBF_DEBUG_FLEX_ANON_METRICS', + defaultValue: false); static int flexAnonymousMetricsProfilingSummaryEvery = - const int.fromEnvironment('WEBF_DEBUG_FLEX_ANON_METRICS_SUMMARY_EVERY', defaultValue: 50); + const int.fromEnvironment('WEBF_DEBUG_FLEX_ANON_METRICS_SUMMARY_EVERY', + defaultValue: 50); static int flexAnonymousMetricsProfilingMaxDetailLogs = - const int.fromEnvironment('WEBF_DEBUG_FLEX_ANON_METRICS_MAX_DETAIL_LOGS', defaultValue: 20); + const int.fromEnvironment('WEBF_DEBUG_FLEX_ANON_METRICS_MAX_DETAIL_LOGS', + defaultValue: 20); static String flexAnonymousMetricsProfilingWatchedPathContains = - const String.fromEnvironment('WEBF_DEBUG_FLEX_ANON_METRICS_WATCH_PATH', defaultValue: ''); + const String.fromEnvironment('WEBF_DEBUG_FLEX_ANON_METRICS_WATCH_PATH', + defaultValue: ''); /// Debug flag to enable inline layout visualization. /// When true, paints debug information for line boxes, margins, padding, etc. diff --git a/webf/lib/src/rendering/flex.dart b/webf/lib/src/rendering/flex.dart index 2f1ce0c006..2789a797d4 100644 --- a/webf/lib/src/rendering/flex.dart +++ b/webf/lib/src/rendering/flex.dart @@ -129,6 +129,185 @@ typedef _FlexFastPathRejectCallback = void Function( Map? details, }); +enum _FlexAdjustFastPathRejectReason { + wouldGrow, + wouldShrink, +} + +String _flexAdjustFastPathRejectReasonLabel( + _FlexAdjustFastPathRejectReason reason) { + switch (reason) { + case _FlexAdjustFastPathRejectReason.wouldGrow: + return 'wouldGrow'; + case _FlexAdjustFastPathRejectReason.wouldShrink: + return 'wouldShrink'; + } +} + +enum _FlexAdjustFastPathRelayoutReason { + effectiveChildNeedsRelayout, + postMeasureLayout, + preservedMainMismatch, + autoMainWithNonTightConstraint, + columnAutoCrossOverflow, +} + +String _flexAdjustFastPathRelayoutReasonLabel( + _FlexAdjustFastPathRelayoutReason reason) { + switch (reason) { + case _FlexAdjustFastPathRelayoutReason.effectiveChildNeedsRelayout: + return 'effectiveChildNeedsRelayout'; + case _FlexAdjustFastPathRelayoutReason.postMeasureLayout: + return 'postMeasureLayout'; + case _FlexAdjustFastPathRelayoutReason.preservedMainMismatch: + return 'preservedMainMismatch'; + case _FlexAdjustFastPathRelayoutReason.autoMainWithNonTightConstraint: + return 'autoMainWithNonTightConstraint'; + case _FlexAdjustFastPathRelayoutReason.columnAutoCrossOverflow: + return 'columnAutoCrossOverflow'; + } +} + +class _FlexAdjustFastPathProfiler { + static int _attempts = 0; + static int _hits = 0; + static int _detailLogs = 0; + static int _relayoutRows = 0; + static int _relayoutChildren = 0; + static final Map<_FlexAdjustFastPathRejectReason, int> _rejectCounts = + <_FlexAdjustFastPathRejectReason, int>{}; + static final Map<_FlexAdjustFastPathRelayoutReason, int> _relayoutCounts = + <_FlexAdjustFastPathRelayoutReason, int>{}; + + static bool get enabled => DebugFlags.enableFlexAdjustFastPathProfiling; + + static int get _summaryEvery { + final int configured = DebugFlags.flexAdjustFastPathProfilingSummaryEvery; + return configured > 0 ? configured : 50; + } + + static int get _maxDetailLogs { + final int configured = DebugFlags.flexAdjustFastPathProfilingMaxDetailLogs; + return configured >= 0 ? configured : 0; + } + + static void recordReject( + String path, + _FlexAdjustFastPathRejectReason reason, { + Map? details, + }) { + if (!enabled) return; + _attempts++; + _rejectCounts.update(reason, (int value) => value + 1, ifAbsent: () => 1); + if (_detailLogs < _maxDetailLogs) { + final StringBuffer message = StringBuffer() + ..write('[FlexAdjustFastPath][reject] path=') + ..write(path) + ..write(' reason=') + ..write(_flexAdjustFastPathRejectReasonLabel(reason)); + if (details != null && details.isNotEmpty) { + message + ..write(' details=') + ..write(_formatDetails(details)); + } + renderingLogger.info(message.toString()); + _detailLogs++; + } + _maybeLogSummary(); + } + + static void recordRelayout( + String path, + String childLabel, + _FlexAdjustFastPathRelayoutReason reason, { + BoxConstraints? childConstraints, + Map? details, + }) { + if (!enabled) return; + _relayoutChildren++; + _relayoutCounts.update(reason, (int value) => value + 1, ifAbsent: () => 1); + if (_detailLogs < _maxDetailLogs) { + final StringBuffer message = StringBuffer() + ..write('[FlexAdjustFastPath][relayout] path=') + ..write(path) + ..write(' child=') + ..write(childLabel) + ..write(' reason=') + ..write(_flexAdjustFastPathRelayoutReasonLabel(reason)); + if (childConstraints != null) { + message + ..write(' constraints=') + ..write(childConstraints); + } + if (details != null && details.isNotEmpty) { + message + ..write(' details=') + ..write(_formatDetails(details)); + } + renderingLogger.info(message.toString()); + _detailLogs++; + } + } + + static void recordHit( + String path, { + required int relayoutRowCount, + required int relayoutChildCount, + }) { + if (!enabled) return; + _attempts++; + _hits++; + _relayoutRows += relayoutRowCount; + _relayoutChildren += relayoutChildCount; + _maybeLogSummary(); + } + + static void _maybeLogSummary() { + if (!enabled) return; + if (_attempts == 0 || _attempts % _summaryEvery != 0) return; + + final int rejects = _attempts - _hits; + final double hitRate = _attempts == 0 ? 0.0 : (_hits / _attempts) * 100.0; + final String rejectSummary = _formatCounts( + _rejectCounts, + _flexAdjustFastPathRejectReasonLabel, + ); + final String relayoutSummary = _formatCounts( + _relayoutCounts, + _flexAdjustFastPathRelayoutReasonLabel, + ); + + renderingLogger.info( + '[FlexAdjustFastPath][summary] attempts=$_attempts hits=$_hits ' + 'hitRate=${hitRate.toStringAsFixed(1)}% rejects=$rejects ' + 'relayoutRows=$_relayoutRows relayoutChildren=$_relayoutChildren ' + 'rejectReasons=$rejectSummary relayoutReasons=$relayoutSummary', + ); + } + + static String _formatCounts( + Map counts, + String Function(T value) labelFor, + ) { + if (counts.isEmpty) { + return 'none'; + } + final List> entries = counts.entries.toList() + ..sort((MapEntry a, MapEntry b) => + b.value.compareTo(a.value)); + return entries + .map( + (MapEntry entry) => '${labelFor(entry.key)}=${entry.value}') + .join(', '); + } + + static String _formatDetails(Map details) { + return details.entries + .map((MapEntry entry) => '${entry.key}=${entry.value}') + .join(', '); + } +} + class _FlexFastPathProfiler { static int _attempts = 0; static int _hits = 0; @@ -250,7 +429,8 @@ class _FlexAnonymousMetricsProfiler { } static int get _maxDetailLogs { - final int configured = DebugFlags.flexAnonymousMetricsProfilingMaxDetailLogs; + final int configured = + DebugFlags.flexAnonymousMetricsProfilingMaxDetailLogs; return configured >= 0 ? configured : 0; } @@ -479,14 +659,15 @@ class _FlexIntrinsicMeasurementLookupResult { // Position and size info of each run (flex line) in flex layout. // https://www.w3.org/TR/css-flexbox-1/#flex-lines class _RunMetrics { - _RunMetrics(this.mainAxisExtent, - this.crossAxisExtent, - double totalFlexGrow, - double totalFlexShrink, - this.baselineExtent, - this.runChildren, - double remainingFreeSpace,) - : _totalFlexGrow = totalFlexGrow, + _RunMetrics( + this.mainAxisExtent, + this.crossAxisExtent, + double totalFlexGrow, + double totalFlexShrink, + this.baselineExtent, + this.runChildren, + double remainingFreeSpace, + ) : _totalFlexGrow = totalFlexGrow, _totalFlexShrink = totalFlexShrink, _remainingFreeSpace = remainingFreeSpace; @@ -535,30 +716,30 @@ class _RunMetrics { // Infos about flex item in the run. class _RunChild { - _RunChild(RenderBox child, - double originalMainSize, - double flexedMainSize, - bool frozen, { - required this.effectiveChild, - required this.alignSelf, - required this.flexGrow, - required this.flexShrink, - required this.usedFlexBasis, - required this.mainAxisMargin, - required this.mainAxisStartMargin, - required this.mainAxisEndMargin, - required this.crossAxisStartMargin, - required this.crossAxisEndMargin, - required this.hasAutoMainAxisMargin, - required this.hasAutoCrossAxisMargin, - required this.marginLeftAuto, - required this.marginRightAuto, - required this.marginTopAuto, - required this.marginBottomAuto, - required this.isReplaced, - required this.aspectRatio, - }) - : _child = child, + _RunChild( + RenderBox child, + double originalMainSize, + double flexedMainSize, + bool frozen, { + required this.effectiveChild, + required this.alignSelf, + required this.flexGrow, + required this.flexShrink, + required this.usedFlexBasis, + required this.mainAxisMargin, + required this.mainAxisStartMargin, + required this.mainAxisEndMargin, + required this.crossAxisStartMargin, + required this.crossAxisEndMargin, + required this.hasAutoMainAxisMargin, + required this.hasAutoCrossAxisMargin, + required this.marginLeftAuto, + required this.marginRightAuto, + required this.marginTopAuto, + required this.marginBottomAuto, + required this.isReplaced, + required this.aspectRatio, + }) : _child = child, _originalMainSize = originalMainSize, _flexedMainSize = flexedMainSize, _unclampedMainSize = originalMainSize, @@ -697,7 +878,8 @@ class _FlexContainerInvariants { factory _FlexContainerInvariants.compute(RenderFlexLayout layout) { final bool isHorizontalFlexDirection = layout._isHorizontalFlexDirection; - final bool isMainAxisStartAtPhysicalStart = layout._isMainAxisStartAtPhysicalStart(); + final bool isMainAxisStartAtPhysicalStart = + layout._isMainAxisStartAtPhysicalStart(); final bool isMainAxisReversed = layout._isMainAxisReversed(); // Determine cross axis orientation and where cross-start maps physically. @@ -706,7 +888,8 @@ class _FlexContainerInvariants { final FlexDirection flexDirection = layout.renderStyle.flexDirection; final bool isCrossAxisHorizontal; final bool isCrossAxisStartAtPhysicalStart; - if (flexDirection == FlexDirection.row || flexDirection == FlexDirection.rowReverse) { + if (flexDirection == FlexDirection.row || + flexDirection == FlexDirection.rowReverse) { // Cross is block axis. isCrossAxisHorizontal = !inlineIsHorizontal; if (isCrossAxisHorizontal) { @@ -721,7 +904,8 @@ class _FlexContainerInvariants { isCrossAxisHorizontal = inlineIsHorizontal; if (isCrossAxisHorizontal) { // Inline-start follows text direction in horizontal-tb. - isCrossAxisStartAtPhysicalStart = (layout.renderStyle.direction != TextDirection.rtl); + isCrossAxisStartAtPhysicalStart = + (layout.renderStyle.direction != TextDirection.rtl); } else { // Inline-start is physical top in vertical writing modes. isCrossAxisStartAtPhysicalStart = true; @@ -853,15 +1037,18 @@ class RenderFlexLayout extends RenderLayoutBox { } final bool mainAxisIsHorizontal = _isHorizontalFlexDirection; - final double gap = _intrinsicMainAxisGap(mainAxisIsHorizontal: mainAxisIsHorizontal); + final double gap = + _intrinsicMainAxisGap(mainAxisIsHorizontal: mainAxisIsHorizontal); double contentWidth = 0.0; int count = 0; RenderBox? child = firstChild; while (child != null) { - final RenderLayoutParentData childParentData = child.parentData as RenderLayoutParentData; + final RenderLayoutParentData childParentData = + child.parentData as RenderLayoutParentData; if (child is! RenderPositionPlaceholder) { - final double w = child.getMinIntrinsicWidth(height) + _childMarginHorizontal(child); + final double w = + child.getMinIntrinsicWidth(height) + _childMarginHorizontal(child); if (mainAxisIsHorizontal) { if (w.isFinite) contentWidth += w; } else { @@ -885,15 +1072,18 @@ class RenderFlexLayout extends RenderLayoutBox { } final bool mainAxisIsHorizontal = _isHorizontalFlexDirection; - final double gap = _intrinsicMainAxisGap(mainAxisIsHorizontal: mainAxisIsHorizontal); + final double gap = + _intrinsicMainAxisGap(mainAxisIsHorizontal: mainAxisIsHorizontal); double contentWidth = 0.0; int count = 0; RenderBox? child = firstChild; while (child != null) { - final RenderLayoutParentData childParentData = child.parentData as RenderLayoutParentData; + final RenderLayoutParentData childParentData = + child.parentData as RenderLayoutParentData; if (child is! RenderPositionPlaceholder) { - final double w = child.getMaxIntrinsicWidth(height) + _childMarginHorizontal(child); + final double w = + child.getMaxIntrinsicWidth(height) + _childMarginHorizontal(child); if (mainAxisIsHorizontal) { if (w.isFinite) contentWidth += w; } else { @@ -917,15 +1107,18 @@ class RenderFlexLayout extends RenderLayoutBox { } final bool mainAxisIsHorizontal = _isHorizontalFlexDirection; - final double gap = _intrinsicMainAxisGap(mainAxisIsHorizontal: !mainAxisIsHorizontal); + final double gap = + _intrinsicMainAxisGap(mainAxisIsHorizontal: !mainAxisIsHorizontal); double contentHeight = 0.0; int count = 0; RenderBox? child = firstChild; while (child != null) { - final RenderLayoutParentData childParentData = child.parentData as RenderLayoutParentData; + final RenderLayoutParentData childParentData = + child.parentData as RenderLayoutParentData; if (child is! RenderPositionPlaceholder) { - final double h = child.getMinIntrinsicHeight(width) + _childMarginVertical(child); + final double h = + child.getMinIntrinsicHeight(width) + _childMarginVertical(child); if (!mainAxisIsHorizontal) { if (h.isFinite) contentHeight += h; } else { @@ -949,15 +1142,18 @@ class RenderFlexLayout extends RenderLayoutBox { } final bool mainAxisIsHorizontal = _isHorizontalFlexDirection; - final double gap = _intrinsicMainAxisGap(mainAxisIsHorizontal: !mainAxisIsHorizontal); + final double gap = + _intrinsicMainAxisGap(mainAxisIsHorizontal: !mainAxisIsHorizontal); double contentHeight = 0.0; int count = 0; RenderBox? child = firstChild; while (child != null) { - final RenderLayoutParentData childParentData = child.parentData as RenderLayoutParentData; + final RenderLayoutParentData childParentData = + child.parentData as RenderLayoutParentData; if (child is! RenderPositionPlaceholder) { - final double h = child.getMaxIntrinsicHeight(width) + _childMarginVertical(child); + final double h = + child.getMaxIntrinsicHeight(width) + _childMarginVertical(child); if (!mainAxisIsHorizontal) { if (h.isFinite) contentHeight += h; } else { @@ -983,10 +1179,13 @@ class RenderFlexLayout extends RenderLayoutBox { // Unwrap wrappers to read padding/border from the flex item element itself. RenderBoxModel? box = child is RenderBoxModel ? child - : (child is RenderEventListener ? child.child as RenderBoxModel? : null); + : (child is RenderEventListener + ? child.child as RenderBoxModel? + : null); if (box == null) return basis; final double paddingBorder = _isHorizontalFlexDirection - ? (box.renderStyle.padding.horizontal + box.renderStyle.border.horizontal) + ? (box.renderStyle.padding.horizontal + + box.renderStyle.border.horizontal) : (box.renderStyle.padding.vertical + box.renderStyle.border.vertical); return math.max(basis, paddingBorder); } @@ -997,12 +1196,15 @@ class RenderFlexLayout extends RenderLayoutBox { // Cache the intrinsic size of children before flex-grow/flex-shrink // to avoid relayout when style of flex items changes. - Expando _childrenIntrinsicMainSizes = Expando('childrenIntrinsicMainSizes'); + Expando _childrenIntrinsicMainSizes = + Expando('childrenIntrinsicMainSizes'); // Cache original constraints of children on the first layout. - Expando _childrenOldConstraints = Expando('childrenOldConstraints'); + Expando _childrenOldConstraints = + Expando('childrenOldConstraints'); Expando<_FlexIntrinsicMeasurementCacheBucket> _childrenIntrinsicMeasureCache = - Expando<_FlexIntrinsicMeasurementCacheBucket>('childrenIntrinsicMeasureCache'); + Expando<_FlexIntrinsicMeasurementCacheBucket>( + 'childrenIntrinsicMeasureCache'); Expando _childrenRequirePostMeasureLayout = Expando('childrenRequirePostMeasureLayout'); Expando? _transientChildSizeOverrides; @@ -1019,7 +1221,8 @@ class RenderFlexLayout extends RenderLayoutBox { _childrenIntrinsicMainSizes = Expando('childrenIntrinsicMainSizes'); _childrenOldConstraints = Expando('childrenOldConstraints'); _childrenIntrinsicMeasureCache = - Expando<_FlexIntrinsicMeasurementCacheBucket>('childrenIntrinsicMeasureCache'); + Expando<_FlexIntrinsicMeasurementCacheBucket>( + 'childrenIntrinsicMeasureCache'); _childrenRequirePostMeasureLayout = Expando('childrenRequirePostMeasureLayout'); _transientChildSizeOverrides = null; @@ -1070,7 +1273,8 @@ class RenderFlexLayout extends RenderLayoutBox { switch (renderStyle.flexDirection) { case FlexDirection.row: if (inlineIsHorizontal) { - return dir != TextDirection.rtl; // LTR → left is start; RTL → right is start + return dir != + TextDirection.rtl; // LTR → left is start; RTL → right is start } else { return true; // vertical inline: top is start } @@ -1081,10 +1285,10 @@ class RenderFlexLayout extends RenderLayoutBox { return false; // vertical inline: bottom is start } case FlexDirection.column: - // Column follows block axis. - // - horizontal-tb: block is vertical (top is start) - // - vertical-rl: block is horizontal (start at physical right) - // - vertical-lr: block is horizontal (start at physical left) + // Column follows block axis. + // - horizontal-tb: block is vertical (top is start) + // - vertical-rl: block is horizontal (start at physical right) + // - vertical-lr: block is horizontal (start at physical left) if (inlineIsHorizontal) { return true; // top is start } else { @@ -1117,20 +1321,29 @@ class RenderFlexLayout extends RenderLayoutBox { // Get start/end padding in the main axis according to flex direction. double _flowAwareMainAxisPadding({bool isEnd = false}) { final _FlexContainerInvariants? inv = _layoutInvariants; - if (inv != null) return isEnd ? inv.mainAxisPaddingEnd : inv.mainAxisPaddingStart; + if (inv != null) + return isEnd ? inv.mainAxisPaddingEnd : inv.mainAxisPaddingStart; if (_isHorizontalFlexDirection) { final bool startIsLeft = _isMainAxisStartAtPhysicalStart(); if (!isEnd) { - return startIsLeft ? renderStyle.paddingLeft.computedValue : renderStyle.paddingRight.computedValue; + return startIsLeft + ? renderStyle.paddingLeft.computedValue + : renderStyle.paddingRight.computedValue; } else { - return startIsLeft ? renderStyle.paddingRight.computedValue : renderStyle.paddingLeft.computedValue; + return startIsLeft + ? renderStyle.paddingRight.computedValue + : renderStyle.paddingLeft.computedValue; } } else { final bool startIsTop = _isMainAxisStartAtPhysicalStart(); if (!isEnd) { - return startIsTop ? renderStyle.paddingTop.computedValue : renderStyle.paddingBottom.computedValue; + return startIsTop + ? renderStyle.paddingTop.computedValue + : renderStyle.paddingBottom.computedValue; } else { - return startIsTop ? renderStyle.paddingBottom.computedValue : renderStyle.paddingTop.computedValue; + return startIsTop + ? renderStyle.paddingBottom.computedValue + : renderStyle.paddingTop.computedValue; } } } @@ -1138,24 +1351,28 @@ class RenderFlexLayout extends RenderLayoutBox { // Get start/end padding in the cross axis according to flex direction. double _flowAwareCrossAxisPadding({bool isEnd = false}) { final _FlexContainerInvariants? inv = _layoutInvariants; - if (inv != null) return isEnd ? inv.crossAxisPaddingEnd : inv.crossAxisPaddingStart; + if (inv != null) + return isEnd ? inv.crossAxisPaddingEnd : inv.crossAxisPaddingStart; // Cross axis comes from block axis for row, inline axis for column final CSSWritingMode wm = renderStyle.writingMode; final bool inlineIsHorizontal = (wm == CSSWritingMode.horizontalTb); final bool crossIsHorizontal; bool crossStartIsPhysicalStart; // left for horizontal, top for vertical - if (renderStyle.flexDirection == FlexDirection.row || renderStyle.flexDirection == FlexDirection.rowReverse) { + if (renderStyle.flexDirection == FlexDirection.row || + renderStyle.flexDirection == FlexDirection.rowReverse) { crossIsHorizontal = !inlineIsHorizontal; // block axis if (crossIsHorizontal) { - crossStartIsPhysicalStart = - (wm == CSSWritingMode.verticalLr); // start at left for vertical-lr, right for vertical-rl + crossStartIsPhysicalStart = (wm == + CSSWritingMode + .verticalLr); // start at left for vertical-lr, right for vertical-rl } else { crossStartIsPhysicalStart = true; // top for horizontal-tb } } else { crossIsHorizontal = inlineIsHorizontal; // inline axis if (crossIsHorizontal) { - crossStartIsPhysicalStart = (renderStyle.direction != TextDirection.rtl); // left if LTR, right if RTL + crossStartIsPhysicalStart = (renderStyle.direction != + TextDirection.rtl); // left if LTR, right if RTL } else { crossStartIsPhysicalStart = true; // top in vertical writing modes } @@ -1172,14 +1389,17 @@ class RenderFlexLayout extends RenderLayoutBox { : renderStyle.paddingLeft.computedValue; } } else { - return isEnd ? renderStyle.paddingBottom.computedValue : renderStyle.paddingTop.computedValue; + return isEnd + ? renderStyle.paddingBottom.computedValue + : renderStyle.paddingTop.computedValue; } } // Get start/end border in the main axis according to flex direction. double _flowAwareMainAxisBorder({bool isEnd = false}) { final _FlexContainerInvariants? inv = _layoutInvariants; - if (inv != null) return isEnd ? inv.mainAxisBorderEnd : inv.mainAxisBorderStart; + if (inv != null) + return isEnd ? inv.mainAxisBorderEnd : inv.mainAxisBorderStart; if (_isHorizontalFlexDirection) { final bool startIsLeft = _isMainAxisStartAtPhysicalStart(); if (!isEnd) { @@ -1208,13 +1428,16 @@ class RenderFlexLayout extends RenderLayoutBox { // Get start/end border in the cross axis according to flex direction. double _flowAwareCrossAxisBorder({bool isEnd = false}) { final _FlexContainerInvariants? inv = _layoutInvariants; - if (inv != null) return isEnd ? inv.crossAxisBorderEnd : inv.crossAxisBorderStart; + if (inv != null) + return isEnd ? inv.crossAxisBorderEnd : inv.crossAxisBorderStart; final CSSWritingMode wm = renderStyle.writingMode; final bool crossIsHorizontal = !_isHorizontalFlexDirection; if (crossIsHorizontal) { - final bool usesBlockAxis = renderStyle.flexDirection == FlexDirection.row || - renderStyle.flexDirection == FlexDirection.rowReverse; - final bool crossStartIsPhysicalLeft = usesBlockAxis ? (wm == CSSWritingMode.verticalLr) : true; + final bool usesBlockAxis = + renderStyle.flexDirection == FlexDirection.row || + renderStyle.flexDirection == FlexDirection.rowReverse; + final bool crossStartIsPhysicalLeft = + usesBlockAxis ? (wm == CSSWritingMode.verticalLr) : true; if (!isEnd) { return crossStartIsPhysicalLeft ? renderStyle.effectiveBorderLeftWidth.computedValue @@ -1269,14 +1492,16 @@ class RenderFlexLayout extends RenderLayoutBox { } double _calculateMainAxisMarginForJustContentType(double margin) { - if (renderStyle.justifyContent == JustifyContent.spaceBetween && margin < 0) { + if (renderStyle.justifyContent == JustifyContent.spaceBetween && + margin < 0) { return margin / 2; } return margin; } // Get start/end margin of child in the cross axis according to flex direction. - double? _flowAwareChildCrossAxisMargin(RenderBox child, {bool isEnd = false}) { + double? _flowAwareChildCrossAxisMargin(RenderBox child, + {bool isEnd = false}) { RenderBoxModel? childRenderBoxModel; if (child is RenderBoxModel) { childRenderBoxModel = child; @@ -1290,8 +1515,9 @@ class RenderFlexLayout extends RenderLayoutBox { final CSSWritingMode wm = renderStyle.writingMode; final bool crossIsHorizontal = !_isHorizontalFlexDirection; if (crossIsHorizontal) { - final bool usesBlockAxis = renderStyle.flexDirection == FlexDirection.row || - renderStyle.flexDirection == FlexDirection.rowReverse; + final bool usesBlockAxis = + renderStyle.flexDirection == FlexDirection.row || + renderStyle.flexDirection == FlexDirection.rowReverse; // When the cross axis is horizontal, it can be either the block axis (in // vertical writing modes for row/row-reverse) or the inline axis (for // column/column-reverse in horizontal writing mode). Inline-start depends @@ -1333,7 +1559,9 @@ class RenderFlexLayout extends RenderLayoutBox { } RenderBoxModel? box = child is RenderBoxModel ? child - : (child is RenderEventListener ? child.child as RenderBoxModel? : null); + : (child is RenderEventListener + ? child.child as RenderBoxModel? + : null); return box != null ? box.renderStyle.flexGrow : 0.0; } @@ -1344,14 +1572,18 @@ class RenderFlexLayout extends RenderLayoutBox { } RenderBoxModel? box = child is RenderBoxModel ? child - : (child is RenderEventListener ? child.child as RenderBoxModel? : null); + : (child is RenderEventListener + ? child.child as RenderBoxModel? + : null); return box != null ? box.renderStyle.flexShrink : 0.0; } double? _getFlexBasis(RenderBox child) { RenderBoxModel? box = child is RenderBoxModel ? child - : (child is RenderEventListener ? child.child as RenderBoxModel? : null); + : (child is RenderEventListener + ? child.child as RenderBoxModel? + : null); if (box != null && box.renderStyle.flexBasis != CSSLengthValue.auto) { // flex-basis: content → base size is content-based; do not return a numeric value here if (box.renderStyle.flexBasis?.type == CSSLengthType.CONTENT) { @@ -1364,7 +1596,9 @@ class RenderFlexLayout extends RenderLayoutBox { /// and if that containing block’s size is indefinite, the used value for flex-basis is content. // Note: When flex-basis is 0%, it should remain 0, not be changed to minContentWidth // The commented code below was incorrectly setting flexBasis to minContentWidth for 0% values - if (flexBasis != null && flexBasis == 0 && box.renderStyle.flexBasis?.type == CSSLengthType.PERCENTAGE) { + if (flexBasis != null && + flexBasis == 0 && + box.renderStyle.flexBasis?.type == CSSLengthType.PERCENTAGE) { // CSS Flexbox: percentage flex-basis is resolved against the flex container’s // inner main size. If that size is indefinite, the used value is 'content'. // Consider explicit sizing, tight constraints, or bounded constraints as definite. @@ -1372,8 +1606,10 @@ class RenderFlexLayout extends RenderLayoutBox { ? (renderStyle.contentBoxLogicalWidth != null) : (renderStyle.contentBoxLogicalHeight != null); final bool mainTight = _isHorizontalFlexDirection - ? ((contentConstraints?.hasTightWidth ?? false) || constraints.hasTightWidth) - : ((contentConstraints?.hasTightHeight ?? false) || constraints.hasTightHeight); + ? ((contentConstraints?.hasTightWidth ?? false) || + constraints.hasTightWidth) + : ((contentConstraints?.hasTightHeight ?? false) || + constraints.hasTightHeight); final bool mainDefinite = hasSpecifiedMain || mainTight; if (!mainDefinite) { return null; @@ -1400,15 +1636,18 @@ class RenderFlexLayout extends RenderLayoutBox { double _getMaxMainAxisSize(RenderBoxModel child) { double? resolvePctCap(CSSLengthValue len) { - if (len.type != CSSLengthType.PERCENTAGE || len.value == null) return null; + if (len.type != CSSLengthType.PERCENTAGE || len.value == null) + return null; // Determine the container's inner content-box size in the main axis. double? containerInner; if (_isHorizontalFlexDirection) { containerInner = renderStyle.contentBoxLogicalWidth; if (containerInner == null) { - if (contentConstraints != null && contentConstraints!.maxWidth.isFinite) { + if (contentConstraints != null && + contentConstraints!.maxWidth.isFinite) { containerInner = contentConstraints!.maxWidth; - } else if (constraints.hasTightWidth && constraints.maxWidth.isFinite) { + } else if (constraints.hasTightWidth && + constraints.maxWidth.isFinite) { containerInner = constraints.maxWidth; } else { // Fallback to ancestor-provided available inline size used for shrink-to-fit. @@ -1419,9 +1658,11 @@ class RenderFlexLayout extends RenderLayoutBox { } else { containerInner = renderStyle.contentBoxLogicalHeight; if (containerInner == null) { - if (contentConstraints != null && contentConstraints!.maxHeight.isFinite) { + if (contentConstraints != null && + contentConstraints!.maxHeight.isFinite) { containerInner = contentConstraints!.maxHeight; - } else if (constraints.hasTightHeight && constraints.maxHeight.isFinite) { + } else if (constraints.hasTightHeight && + constraints.maxHeight.isFinite) { containerInner = constraints.maxHeight; } } @@ -1455,9 +1696,13 @@ class RenderFlexLayout extends RenderLayoutBox { if (child is RenderBoxModel) { double autoMinSize = _getAutoMinSize(child); if (_isHorizontalFlexDirection) { - minMainSize = child.renderStyle.minWidth.isAuto ? autoMinSize : child.renderStyle.minWidth.computedValue; + minMainSize = child.renderStyle.minWidth.isAuto + ? autoMinSize + : child.renderStyle.minWidth.computedValue; } else { - minMainSize = child.renderStyle.minHeight.isAuto ? autoMinSize : child.renderStyle.minHeight.computedValue; + minMainSize = child.renderStyle.minHeight.isAuto + ? autoMinSize + : child.renderStyle.minHeight.computedValue; } } @@ -1473,11 +1718,13 @@ class RenderFlexLayout extends RenderLayoutBox { if (child is RenderTextBox) { // RenderEventListener directly contains a text box - mark it for flex relayout _setFlexRelayoutForTextParent(boxModel); - } else if (child is RenderLayoutBox) { + } else if (child is RenderLayoutBox && + _hasOnlyTextFlexRelayoutSubtree(child)) { // RenderEventListener contains a layout box - check if that layout box only contains text _markRenderLayoutBoxForTextOnly(child); } - } else if (boxModel is RenderLayoutBox) { + } else if (boxModel is RenderLayoutBox && + _hasOnlyTextFlexRelayoutSubtree(boxModel)) { // Check if this layout box only contains text _markRenderLayoutBoxForTextOnly(boxModel); } @@ -1488,6 +1735,10 @@ class RenderFlexLayout extends RenderLayoutBox { // preventing constraint violations when the flex container adjusts item sizes. // Only apply when the flex container itself has indefinite width. void _markRenderLayoutBoxForTextOnly(RenderLayoutBox layoutBox) { + if (!_hasOnlyTextFlexRelayoutSubtree(layoutBox)) { + return; + } + if (layoutBox.childCount == 1) { RenderObject? firstChild = layoutBox.firstChild; if (firstChild is RenderEventListener) { @@ -1503,6 +1754,25 @@ class RenderFlexLayout extends RenderLayoutBox { } } + bool _hasOnlyTextFlexRelayoutSubtree(RenderObject? node) { + if (node == null) { + return false; + } + if (node is RenderTextBox) { + return true; + } + if (node is RenderEventListener) { + return _hasOnlyTextFlexRelayoutSubtree(node.child); + } + if (node is RenderLayoutBox) { + if (node.childCount != 1) { + return false; + } + return _hasOnlyTextFlexRelayoutSubtree(node.firstChild); + } + return false; + } + void _setFlexRelayoutForTextParent(RenderBoxModel textParentBoxModel) { if (textParentBoxModel.renderStyle.display == CSSDisplay.flex && textParentBoxModel.renderStyle.width.isAuto && @@ -1533,13 +1803,16 @@ class RenderFlexLayout extends RenderLayoutBox { // (clamped by its max main size property if it’s definite). It is otherwise undefined. // https://www.w3.org/TR/css-flexbox-1/#specified-size-suggestion double? specifiedSize; - final CSSLengthValue mainSize = _isHorizontalFlexDirection ? childRenderStyle.width : childRenderStyle.height; + final CSSLengthValue mainSize = _isHorizontalFlexDirection + ? childRenderStyle.width + : childRenderStyle.height; if (!mainSize.isIntrinsic && mainSize.isNotAuto) { if (mainSize.type == CSSLengthType.PERCENTAGE) { // Percentage main sizes resolve against the flex container and may be // indefinite until layout. Use the already-resolved logical size when // available. - specifiedSize = _isHorizontalFlexDirection ? childLogicalWidth : childLogicalHeight; + specifiedSize = + _isHorizontalFlexDirection ? childLogicalWidth : childLogicalHeight; } else { // Avoid using `contentBoxLogicalWidth/Height` here: those values can be // overridden by flex sizing (grow/shrink) and would make the "specified @@ -1550,7 +1823,8 @@ class RenderFlexLayout extends RenderLayoutBox { double contentBoxMain = _isHorizontalFlexDirection ? childRenderStyle.deflatePaddingBorderWidth(borderBoxMain) : childRenderStyle.deflatePaddingBorderHeight(borderBoxMain); - if (!contentBoxMain.isFinite || contentBoxMain < 0) contentBoxMain = 0; + if (!contentBoxMain.isFinite || contentBoxMain < 0) + contentBoxMain = 0; specifiedSize = contentBoxMain; } } @@ -1582,8 +1856,10 @@ class RenderFlexLayout extends RenderLayoutBox { double contentSize; if (_isHorizontalFlexDirection) { if (child is RenderFlowLayout && child.inlineFormattingContext != null) { - final double ifcMin = child.inlineFormattingContext!.paragraphMinIntrinsicWidth; - contentSize = (ifcMin.isFinite && ifcMin > 0) ? ifcMin : child.minContentWidth; + final double ifcMin = + child.inlineFormattingContext!.paragraphMinIntrinsicWidth; + contentSize = + (ifcMin.isFinite && ifcMin > 0) ? ifcMin : child.minContentWidth; } else { contentSize = child.minContentWidth; } @@ -1598,7 +1874,9 @@ class RenderFlexLayout extends RenderLayoutBox { } } - CSSLengthValue childCrossSize = _isHorizontalFlexDirection ? childRenderStyle.height : childRenderStyle.width; + CSSLengthValue childCrossSize = _isHorizontalFlexDirection + ? childRenderStyle.height + : childRenderStyle.width; if (childCrossSize.isNotAuto && transferredSize != null) { contentSize = transferredSize; @@ -1611,15 +1889,19 @@ class RenderFlexLayout extends RenderLayoutBox { if (childAspectRatio != null) { if (_isHorizontalFlexDirection) { if (childRenderStyle.minHeight.isNotAuto) { - transferredMinSize = childRenderStyle.minHeight.computedValue * childAspectRatio; + transferredMinSize = + childRenderStyle.minHeight.computedValue * childAspectRatio; } else if (childRenderStyle.maxHeight.isNotNone) { - transferredMaxSize = childRenderStyle.maxHeight.computedValue * childAspectRatio; + transferredMaxSize = + childRenderStyle.maxHeight.computedValue * childAspectRatio; } } else if (!_isHorizontalFlexDirection) { if (childRenderStyle.minWidth.isNotAuto) { - transferredMinSize = childRenderStyle.minWidth.computedValue / childAspectRatio; + transferredMinSize = + childRenderStyle.minWidth.computedValue / childAspectRatio; } else if (childRenderStyle.maxWidth.isNotNone) { - transferredMaxSize = childRenderStyle.maxWidth.computedValue * childAspectRatio; + transferredMaxSize = + childRenderStyle.maxWidth.computedValue * childAspectRatio; } } } @@ -1632,19 +1914,23 @@ class RenderFlexLayout extends RenderLayoutBox { contentSize = transferredMaxSize; } - double? crossSize = - _isHorizontalFlexDirection ? renderStyle.contentBoxLogicalHeight : renderStyle.contentBoxLogicalWidth; + double? crossSize = _isHorizontalFlexDirection + ? renderStyle.contentBoxLogicalHeight + : renderStyle.contentBoxLogicalWidth; // Content size suggestion of replaced flex item will use the cross axis preferred size which came from flexbox's // fixed cross size in newer version of Blink and Gecko which is different from the behavior of WebKit. // https://github.com/w3c/csswg-drafts/issues/6693 - bool isChildCrossSizeStretched = _needToStretchChildCrossSize(child) && crossSize != null; + bool isChildCrossSizeStretched = + _needToStretchChildCrossSize(child) && crossSize != null; if (isChildCrossSizeStretched && transferredSize != null) { contentSize = transferredSize; } - CSSLengthValue maxMainLength = _isHorizontalFlexDirection ? childRenderStyle.maxWidth : childRenderStyle.maxHeight; + CSSLengthValue maxMainLength = _isHorizontalFlexDirection + ? childRenderStyle.maxWidth + : childRenderStyle.maxHeight; // Further clamped by the max main size property if that is definite. if (maxMainLength.isNotNone) { @@ -1676,17 +1962,22 @@ class RenderFlexLayout extends RenderLayoutBox { // Convert the content-box minimum to a border-box minimum by adding padding and border // on the flex container's main axis, per CSS sizing. final double paddingBorderMain = _isHorizontalFlexDirection - ? (childRenderStyle.padding.horizontal + childRenderStyle.border.horizontal) - : (childRenderStyle.padding.vertical + childRenderStyle.border.vertical); + ? (childRenderStyle.padding.horizontal + + childRenderStyle.border.horizontal) + : (childRenderStyle.padding.vertical + + childRenderStyle.border.vertical); // If overflow in the flex main axis is not visible, browsers allow the flex item // to shrink below the content-based minimum. Model this by treating the automatic // minimum as zero (border-box becomes just padding+border). - if ((_isHorizontalFlexDirection && childRenderStyle.overflowX != CSSOverflowType.visible) || - (!_isHorizontalFlexDirection && childRenderStyle.overflowY != CSSOverflowType.visible)) { + if ((_isHorizontalFlexDirection && + childRenderStyle.overflowX != CSSOverflowType.visible) || + (!_isHorizontalFlexDirection && + childRenderStyle.overflowY != CSSOverflowType.visible)) { double autoMinBorderBox = paddingBorderMain; if (maxMainLength.isNotNone) { - autoMinBorderBox = math.min(autoMinBorderBox, maxMainLength.computedValue); + autoMinBorderBox = + math.min(autoMinBorderBox, maxMainLength.computedValue); } return autoMinBorderBox; } @@ -1695,7 +1986,8 @@ class RenderFlexLayout extends RenderLayoutBox { // Finally, clamp by the definite max main size (which is border-box) if present. if (maxMainLength.isNotNone) { - autoMinBorderBox = math.min(autoMinBorderBox, maxMainLength.computedValue); + autoMinBorderBox = + math.min(autoMinBorderBox, maxMainLength.computedValue); } return autoMinBorderBox; @@ -1716,9 +2008,11 @@ class RenderFlexLayout extends RenderLayoutBox { child = child.child as RenderBoxModel; } if (_isPlaceholderPositioned(child)) { - RenderBoxModel? positionedBox = (child as RenderPositionPlaceholder).positioned; + RenderBoxModel? positionedBox = + (child as RenderPositionPlaceholder).positioned; if (positionedBox != null && positionedBox.hasSize == true) { - Size realDisplayedBoxSize = positionedBox.getBoxSize(positionedBox.contentSize); + Size realDisplayedBoxSize = + positionedBox.getBoxSize(positionedBox.contentSize); return BoxConstraints( minWidth: realDisplayedBoxSize.width, maxWidth: realDisplayedBoxSize.width, @@ -1751,7 +2045,8 @@ class RenderFlexLayout extends RenderLayoutBox { // This prevents items from measuring at full container width/height. // Exception: replaced elements (e.g., ) should not be relaxed to ∞, // otherwise they pick viewport-sized widths; keep their container-bounded constraints. - final bool isFlexBasisContent = s.flexBasis?.type == CSSLengthType.CONTENT; + final bool isFlexBasisContent = + s.flexBasis?.type == CSSLengthType.CONTENT; final bool isReplaced = s.isSelfRenderReplaced(); if (_isHorizontalFlexDirection) { // Row direction: main axis is width. For intrinsic measurement, avoid @@ -1760,12 +2055,12 @@ class RenderFlexLayout extends RenderLayoutBox { if (!isReplaced && (s.width.isAuto || isFlexBasisContent)) { // Relax the minimum width to the element's own border-box minimum, // not the parent-imposed tight width, so shrink-to-fit can occur. - double minBorderBoxW = - s.effectiveBorderLeftWidth.computedValue + - s.effectiveBorderRightWidth.computedValue + - s.paddingLeft.computedValue + - s.paddingRight.computedValue; - if (s.minWidth.isNotAuto && s.minWidth.type != CSSLengthType.PERCENTAGE) { + double minBorderBoxW = s.effectiveBorderLeftWidth.computedValue + + s.effectiveBorderRightWidth.computedValue + + s.paddingLeft.computedValue + + s.paddingRight.computedValue; + if (s.minWidth.isNotAuto && + s.minWidth.type != CSSLengthType.PERCENTAGE) { minBorderBoxW = math.max(minBorderBoxW, s.minWidth.computedValue); } c = BoxConstraints( @@ -1781,12 +2076,12 @@ class RenderFlexLayout extends RenderLayoutBox { // when the item has auto height or flex-basis:content. This lets the // item size to its content instead of being prematurely clamped to 0. if (!isReplaced && (s.height.isAuto || isFlexBasisContent)) { - double minBorderBoxH = - s.effectiveBorderTopWidth.computedValue + - s.effectiveBorderBottomWidth.computedValue + - s.paddingTop.computedValue + - s.paddingBottom.computedValue; - if (s.minHeight.isNotAuto && s.minHeight.type != CSSLengthType.PERCENTAGE) { + double minBorderBoxH = s.effectiveBorderTopWidth.computedValue + + s.effectiveBorderBottomWidth.computedValue + + s.paddingTop.computedValue + + s.paddingBottom.computedValue; + if (s.minHeight.isNotAuto && + s.minHeight.type != CSSLengthType.PERCENTAGE) { minBorderBoxH = math.max(minBorderBoxH, s.minHeight.computedValue); } c = BoxConstraints( @@ -1804,8 +2099,11 @@ class RenderFlexLayout extends RenderLayoutBox { if (!isReplaced && (s.width.isAuto || isFlexBasisContent)) { // Determine if child should be stretched in cross axis. final AlignSelf self = s.alignSelf; - final bool parentStretch = renderStyle.alignItems == AlignItems.stretch; - final bool shouldStretch = self == AlignSelf.auto ? parentStretch : self == AlignSelf.stretch; + final bool parentStretch = + renderStyle.alignItems == AlignItems.stretch; + final bool shouldStretch = self == AlignSelf.auto + ? parentStretch + : self == AlignSelf.stretch; // Determine if the container has a definite cross size (width). // Determine whether the flex container's cross size (width in column direction) @@ -1816,12 +2114,14 @@ class RenderFlexLayout extends RenderLayoutBox { // container is not inline-flex with auto width (inline-flex shrink-to-fit should // be treated as indefinite during intrinsic measurement). final bool isInlineFlexAuto = - renderStyle.effectiveDisplay == CSSDisplay.inlineFlex && renderStyle.width.isAuto; + renderStyle.effectiveDisplay == CSSDisplay.inlineFlex && + renderStyle.width.isAuto; final bool containerCrossDefinite = (renderStyle.contentBoxLogicalWidth != null) || (contentConstraints?.hasTightWidth ?? false) || (constraints.hasTightWidth && !isInlineFlexAuto) || - (((contentConstraints?.hasBoundedWidth ?? false) || constraints.hasBoundedWidth) && + (((contentConstraints?.hasBoundedWidth ?? false) || + constraints.hasBoundedWidth) && !isInlineFlexAuto); double newMaxW; @@ -1831,11 +2131,13 @@ class RenderFlexLayout extends RenderLayoutBox { double boundedContainerW = double.infinity; if (constraints.hasTightWidth && constraints.maxWidth.isFinite) { boundedContainerW = constraints.maxWidth; - } else if ((contentConstraints?.hasTightWidth ?? false) && contentConstraints!.maxWidth.isFinite) { + } else if ((contentConstraints?.hasTightWidth ?? false) && + contentConstraints!.maxWidth.isFinite) { boundedContainerW = contentConstraints!.maxWidth; } else if (!isInlineFlexAuto) { // Fall back to bounded (non-tight) width for block-level flex containers. - if ((contentConstraints?.hasBoundedWidth ?? false) && contentConstraints!.maxWidth.isFinite) { + if ((contentConstraints?.hasBoundedWidth ?? false) && + contentConstraints!.maxWidth.isFinite) { boundedContainerW = contentConstraints!.maxWidth; } } @@ -1852,7 +2154,8 @@ class RenderFlexLayout extends RenderLayoutBox { newMaxW = double.infinity; } // Also honor the child's own definite max-width (non-percentage) if any. - if (s.maxWidth.isNotNone && s.maxWidth.type != CSSLengthType.PERCENTAGE) { + if (s.maxWidth.isNotNone && + s.maxWidth.type != CSSLengthType.PERCENTAGE) { newMaxW = math.min(newMaxW, s.maxWidth.computedValue); } @@ -1887,7 +2190,9 @@ class RenderFlexLayout extends RenderLayoutBox { // when that size is effectively indefinite for intrinsic measurement, using 0 // would prematurely collapse the child. Let content sizing drive the measure. final RenderBoxModel childBox = child; - final bool isZeroPctBasis = childBox.renderStyle.flexBasis?.type == CSSLengthType.PERCENTAGE && basis == 0; + final bool isZeroPctBasis = + childBox.renderStyle.flexBasis?.type == CSSLengthType.PERCENTAGE && + basis == 0; // Only skip clamping to 0 for percentage flex-basis during intrinsic sizing // in column direction (vertical main axis). In row direction, a 0% flex-basis // should remain 0 for intrinsic measurement so width-based distribution matches CSS. @@ -1897,8 +2202,8 @@ class RenderFlexLayout extends RenderLayoutBox { // Flex-basis is a definite length. Honor box-sizing:border-box semantics: // the used border-box size cannot be smaller than padding+border. if (_isHorizontalFlexDirection) { - final double minBorderBoxW = - child.renderStyle.padding.horizontal + child.renderStyle.border.horizontal; + final double minBorderBoxW = child.renderStyle.padding.horizontal + + child.renderStyle.border.horizontal; final double used = math.max(basis, minBorderBoxW); c = BoxConstraints( minWidth: used, @@ -1907,8 +2212,8 @@ class RenderFlexLayout extends RenderLayoutBox { maxHeight: c.maxHeight, ); } else { - final double minBorderBoxH = - child.renderStyle.padding.vertical + child.renderStyle.border.vertical; + final double minBorderBoxH = child.renderStyle.padding.vertical + + child.renderStyle.border.vertical; final double used = math.max(basis, minBorderBoxH); c = BoxConstraints( minWidth: c.minWidth, @@ -1940,8 +2245,9 @@ class RenderFlexLayout extends RenderLayoutBox { } if (childRenderBoxModel != null) { - marginHorizontal = childRenderBoxModel.renderStyle.marginLeft.computedValue + - childRenderBoxModel.renderStyle.marginRight.computedValue; + marginHorizontal = + childRenderBoxModel.renderStyle.marginLeft.computedValue + + childRenderBoxModel.renderStyle.marginRight.computedValue; marginVertical = childRenderBoxModel.renderStyle.marginTop.computedValue + childRenderBoxModel.renderStyle.marginBottom.computedValue; } @@ -1958,7 +2264,8 @@ class RenderFlexLayout extends RenderLayoutBox { } } - double _horizontalMarginNegativeSet(double baseSize, RenderBoxModel box, {bool isHorizontal = false}) { + double _horizontalMarginNegativeSet(double baseSize, RenderBoxModel box, + {bool isHorizontal = false}) { CSSRenderStyle boxStyle = box.renderStyle; double? marginLeft = boxStyle.marginLeft.computedValue; double? marginRight = boxStyle.marginRight.computedValue; @@ -1980,7 +2287,8 @@ class RenderFlexLayout extends RenderLayoutBox { return baseSize + box.renderStyle.margin.vertical; } - double _getMainAxisExtent(RenderBox child, {bool shouldUseIntrinsicMainSize = false}) { + double _getMainAxisExtent(RenderBox child, + {bool shouldUseIntrinsicMainSize = false}) { double marginHorizontal = 0; double marginVertical = 0; @@ -1994,13 +2302,15 @@ class RenderFlexLayout extends RenderLayoutBox { } if (childRenderBoxModel != null) { - marginHorizontal = childRenderBoxModel.renderStyle.marginLeft.computedValue + - childRenderBoxModel.renderStyle.marginRight.computedValue; + marginHorizontal = + childRenderBoxModel.renderStyle.marginLeft.computedValue + + childRenderBoxModel.renderStyle.marginRight.computedValue; marginVertical = childRenderBoxModel.renderStyle.marginTop.computedValue + childRenderBoxModel.renderStyle.marginBottom.computedValue; } - double baseSize = _getMainSize(child, shouldUseIntrinsicMainSize: shouldUseIntrinsicMainSize); + double baseSize = _getMainSize(child, + shouldUseIntrinsicMainSize: shouldUseIntrinsicMainSize); if (_isHorizontalFlexDirection) { if (child is RenderLayoutBox && child.isNegativeMarginChangeHSize) { return _horizontalMarginNegativeSet(baseSize, child); @@ -2011,8 +2321,10 @@ class RenderFlexLayout extends RenderLayoutBox { } } - double _getMainSize(RenderBox child, {bool shouldUseIntrinsicMainSize = false}) { - Size? childSize = _getChildSize(child, shouldUseIntrinsicMainSize: shouldUseIntrinsicMainSize); + double _getMainSize(RenderBox child, + {bool shouldUseIntrinsicMainSize = false}) { + Size? childSize = _getChildSize(child, + shouldUseIntrinsicMainSize: shouldUseIntrinsicMainSize); if (_isHorizontalFlexDirection) { return childSize!.width; } else { @@ -2024,9 +2336,8 @@ class RenderFlexLayout extends RenderLayoutBox { double _getMainAxisGap() { final _FlexContainerInvariants? inv = _layoutInvariants; if (inv != null) return inv.mainAxisGap; - CSSLengthValue gap = _isHorizontalFlexDirection - ? renderStyle.columnGap - : renderStyle.rowGap; + CSSLengthValue gap = + _isHorizontalFlexDirection ? renderStyle.columnGap : renderStyle.rowGap; if (gap.type == CSSLengthType.NORMAL) return 0; return gap.computedValue; } @@ -2035,9 +2346,8 @@ class RenderFlexLayout extends RenderLayoutBox { double _getCrossAxisGap() { final _FlexContainerInvariants? inv = _layoutInvariants; if (inv != null) return inv.crossAxisGap; - CSSLengthValue gap = _isHorizontalFlexDirection - ? renderStyle.rowGap - : renderStyle.columnGap; + CSSLengthValue gap = + _isHorizontalFlexDirection ? renderStyle.rowGap : renderStyle.columnGap; if (gap.type == CSSLengthType.NORMAL) return 0; return gap.computedValue; } @@ -2077,9 +2387,12 @@ class RenderFlexLayout extends RenderLayoutBox { ); items.sort((_OrderedFlexItem a, _OrderedFlexItem b) { final int byOrder = a.order.compareTo(b.order); - return byOrder != 0 ? byOrder : a.originalIndex.compareTo(b.originalIndex); + return byOrder != 0 + ? byOrder + : a.originalIndex.compareTo(b.originalIndex); }); - return List.generate(items.length, (int i) => items[i].child, growable: false); + return List.generate(items.length, (int i) => items[i].child, + growable: false); } _FlexResolutionInputs _computeFlexResolutionInputs() { @@ -2096,24 +2409,28 @@ class RenderFlexLayout extends RenderLayoutBox { double? containerHeight; if (containerWidth == null) { - if ((contentConstraints?.hasBoundedWidth ?? false) && (contentConstraints?.maxWidth.isFinite ?? false)) { + if ((contentConstraints?.hasBoundedWidth ?? false) && + (contentConstraints?.maxWidth.isFinite ?? false)) { containerWidth = contentConstraints!.maxWidth; } else if (constraints.hasBoundedWidth && constraints.maxWidth.isFinite) { containerWidth = constraints.maxWidth; } } if (constraints.hasBoundedWidth && constraints.maxWidth.isFinite) { - containerWidth = - (containerWidth == null) ? constraints.maxWidth : math.min(containerWidth, constraints.maxWidth); + containerWidth = (containerWidth == null) + ? constraints.maxWidth + : math.min(containerWidth, constraints.maxWidth); } - if ((contentConstraints?.hasBoundedHeight ?? false) && (contentConstraints?.maxHeight.isFinite ?? false)) { + if ((contentConstraints?.hasBoundedHeight ?? false) && + (contentConstraints?.maxHeight.isFinite ?? false)) { containerHeight = contentConstraints!.maxHeight; } else if (constraints.hasBoundedHeight && constraints.maxHeight.isFinite) { containerHeight = constraints.maxHeight; } if (constraints.hasBoundedHeight && constraints.maxHeight.isFinite) { - containerHeight = - (containerHeight == null) ? constraints.maxHeight : math.min(containerHeight, constraints.maxHeight); + containerHeight = (containerHeight == null) + ? constraints.maxHeight + : math.min(containerHeight, constraints.maxHeight); } if (contentBoxLogicalHeight != null) { containerHeight = contentBoxLogicalHeight; @@ -2123,14 +2440,18 @@ class RenderFlexLayout extends RenderLayoutBox { final double? maxMainSize = isHorizontal ? containerWidth : containerHeight; final bool isMainSizeDefinite = isHorizontal - ? (contentBoxLogicalWidth != null || (contentConstraints?.hasTightWidth ?? false) || - constraints.hasTightWidth || - ((contentConstraints?.hasBoundedWidth ?? false) && (contentConstraints?.maxWidth.isFinite ?? false)) || - (constraints.hasBoundedWidth && constraints.maxWidth.isFinite)) - : (contentBoxLogicalHeight != null || (contentConstraints?.hasTightHeight ?? false) || - constraints.hasTightHeight || - ((contentConstraints?.hasBoundedHeight ?? false) && (contentConstraints?.maxHeight.isFinite ?? false)) || - (constraints.hasBoundedHeight && constraints.maxHeight.isFinite)); + ? (contentBoxLogicalWidth != null || + (contentConstraints?.hasTightWidth ?? false) || + constraints.hasTightWidth || + ((contentConstraints?.hasBoundedWidth ?? false) && + (contentConstraints?.maxWidth.isFinite ?? false)) || + (constraints.hasBoundedWidth && constraints.maxWidth.isFinite)) + : (contentBoxLogicalHeight != null || + (contentConstraints?.hasTightHeight ?? false) || + constraints.hasTightHeight || + ((contentConstraints?.hasBoundedHeight ?? false) && + (contentConstraints?.maxHeight.isFinite ?? false)) || + (constraints.hasBoundedHeight && constraints.maxHeight.isFinite)); return _FlexResolutionInputs( contentBoxLogicalWidth: contentBoxLogicalWidth, @@ -2141,17 +2462,22 @@ class RenderFlexLayout extends RenderLayoutBox { } _RunChild _createRunChildMetadata(RenderBox child, double originalMainSize, - {required RenderBoxModel? effectiveChild, required double? usedFlexBasis}) { + {required RenderBoxModel? effectiveChild, + required double? usedFlexBasis}) { double mainAxisMargin = 0.0; if (effectiveChild != null) { final RenderStyle s = effectiveChild.renderStyle; - final double marginHorizontal = s.marginLeft.computedValue + s.marginRight.computedValue; - final double marginVertical = s.marginTop.computedValue + s.marginBottom.computedValue; - mainAxisMargin = _isHorizontalFlexDirection ? marginHorizontal : marginVertical; + final double marginHorizontal = + s.marginLeft.computedValue + s.marginRight.computedValue; + final double marginVertical = + s.marginTop.computedValue + s.marginBottom.computedValue; + mainAxisMargin = + _isHorizontalFlexDirection ? marginHorizontal : marginVertical; } - final RenderBoxModel? marginBoxModel = - child is RenderBoxModel ? child : (child is RenderPositionPlaceholder ? child.positioned : null); + final RenderBoxModel? marginBoxModel = child is RenderBoxModel + ? child + : (child is RenderPositionPlaceholder ? child.positioned : null); final RenderStyle? marginStyle = marginBoxModel?.renderStyle; final bool marginLeftAuto = marginStyle?.marginLeft.isAuto ?? false; final bool marginRightAuto = marginStyle?.marginRight.isAuto ?? false; @@ -2179,9 +2505,11 @@ class RenderFlexLayout extends RenderLayoutBox { usedFlexBasis: usedFlexBasis, mainAxisMargin: mainAxisMargin, mainAxisStartMargin: _flowAwareChildMainAxisMargin(child) ?? 0.0, - mainAxisEndMargin: _flowAwareChildMainAxisMargin(child, isEnd: true) ?? 0.0, + mainAxisEndMargin: + _flowAwareChildMainAxisMargin(child, isEnd: true) ?? 0.0, crossAxisStartMargin: _flowAwareChildCrossAxisMargin(child) ?? 0.0, - crossAxisEndMargin: _flowAwareChildCrossAxisMargin(child, isEnd: true) ?? 0.0, + crossAxisEndMargin: + _flowAwareChildCrossAxisMargin(child, isEnd: true) ?? 0.0, hasAutoMainAxisMargin: hasAutoMainAxisMargin, hasAutoCrossAxisMargin: hasAutoCrossAxisMargin, marginLeftAuto: marginLeftAuto, @@ -2193,14 +2521,19 @@ class RenderFlexLayout extends RenderLayoutBox { ); } - void _cacheOriginalConstraintsIfNeeded(RenderBox child, BoxConstraints appliedConstraints) { + void _cacheOriginalConstraintsIfNeeded( + RenderBox child, BoxConstraints appliedConstraints) { RenderBoxModel? box = child is RenderBoxModel ? child - : (child is RenderEventListener ? child.child as RenderBoxModel? : null); + : (child is RenderEventListener + ? child.child as RenderBoxModel? + : null); if (box == null) return; - bool hasPercentageMaxWidth = box.renderStyle.maxWidth.type == CSSLengthType.PERCENTAGE; - bool hasPercentageMaxHeight = box.renderStyle.maxHeight.type == CSSLengthType.PERCENTAGE; + bool hasPercentageMaxWidth = + box.renderStyle.maxWidth.type == CSSLengthType.PERCENTAGE; + bool hasPercentageMaxHeight = + box.renderStyle.maxHeight.type == CSSLengthType.PERCENTAGE; if (hasPercentageMaxWidth || hasPercentageMaxHeight) { _childrenOldConstraints[box] = appliedConstraints; @@ -2295,7 +2628,8 @@ class RenderFlexLayout extends RenderLayoutBox { return true; } final AlignSelf alignSelf = _getAlignSelf(child); - return alignSelf == AlignSelf.baseline || alignSelf == AlignSelf.lastBaseline; + return alignSelf == AlignSelf.baseline || + alignSelf == AlignSelf.lastBaseline; } bool _subtreeHasPendingIntrinsicMeasureInvalidation(RenderBox root) { @@ -2307,7 +2641,8 @@ class RenderFlexLayout extends RenderLayoutBox { return true; } - if (root is ContainerRenderObjectMixin>) { + if (root is ContainerRenderObjectMixin>) { RenderBox? child = (root as dynamic).firstChild as RenderBox?; while (child != null) { if (_subtreeHasPendingIntrinsicMeasureInvalidation(child)) { @@ -2360,7 +2695,8 @@ class RenderFlexLayout extends RenderLayoutBox { }; } - if (root is ContainerRenderObjectMixin>) { + if (root is ContainerRenderObjectMixin>) { RenderBox? child = (root as dynamic).firstChild as RenderBox?; while (child != null) { final Map? details = @@ -2390,8 +2726,8 @@ class RenderFlexLayout extends RenderLayoutBox { root.clearIntrinsicMeasurementInvalidationAfterMeasurement(); } - if (root - is ContainerRenderObjectMixin>) { + if (root is ContainerRenderObjectMixin>) { RenderBox? child = (root as dynamic).firstChild as RenderBox?; while (child != null) { _clearSubtreeIntrinsicMeasurementInvalidationAfterMeasurement(child); @@ -2436,8 +2772,7 @@ class RenderFlexLayout extends RenderLayoutBox { BoxConstraints childConstraints, { RenderFlowLayout? flowChild, }) { - final RenderFlowLayout? effectiveFlowChild = - flowChild ?? + final RenderFlowLayout? effectiveFlowChild = flowChild ?? _getCacheableIntrinsicMeasureFlowChild( child, allowAnonymous: true, @@ -2448,7 +2783,8 @@ class RenderFlexLayout extends RenderLayoutBox { int hash = 0; final CSSRenderStyle style = effectiveFlowChild.renderStyle; - hash = _hashReusableIntrinsicMeasurementState(hash, effectiveFlowChild.hashCode); + hash = _hashReusableIntrinsicMeasurementState( + hash, effectiveFlowChild.hashCode); hash = _hashReusableIntrinsicMeasurementState( hash, _quantizeReusableIntrinsicMeasurementDouble(childConstraints.minWidth), @@ -2466,11 +2802,16 @@ class RenderFlexLayout extends RenderLayoutBox { _quantizeReusableIntrinsicMeasurementDouble(childConstraints.maxHeight), ); hash = _hashReusableIntrinsicMeasurementState(hash, style.display.hashCode); - hash = _hashReusableIntrinsicMeasurementState(hash, style.position.hashCode); - hash = _hashReusableIntrinsicMeasurementState(hash, style.whiteSpace.hashCode); - hash = _hashReusableIntrinsicMeasurementState(hash, style.wordBreak.hashCode); - hash = _hashReusableIntrinsicMeasurementState(hash, style.textAlign.hashCode); - hash = _hashReusableIntrinsicMeasurementState(hash, style.fontStyle.hashCode); + hash = + _hashReusableIntrinsicMeasurementState(hash, style.position.hashCode); + hash = + _hashReusableIntrinsicMeasurementState(hash, style.whiteSpace.hashCode); + hash = + _hashReusableIntrinsicMeasurementState(hash, style.wordBreak.hashCode); + hash = + _hashReusableIntrinsicMeasurementState(hash, style.textAlign.hashCode); + hash = + _hashReusableIntrinsicMeasurementState(hash, style.fontStyle.hashCode); hash = _hashReusableIntrinsicMeasurementState(hash, style.fontWeight.value); hash = _hashReusableIntrinsicMeasurementState( hash, @@ -2478,18 +2819,26 @@ class RenderFlexLayout extends RenderLayoutBox { ); hash = _hashReusableIntrinsicMeasurementState( hash, - _quantizeReusableIntrinsicMeasurementDouble(style.lineHeight.computedValue), + _quantizeReusableIntrinsicMeasurementDouble( + style.lineHeight.computedValue), ); hash = _hashReusableIntrinsicMeasurementState( hash, - _quantizeReusableIntrinsicMeasurementDouble(style.textIndent.computedValue), + _quantizeReusableIntrinsicMeasurementDouble( + style.textIndent.computedValue), ); - hash = _hashReusableIntrinsicMeasurementState(hash, style.width.type.hashCode); - hash = _hashReusableIntrinsicMeasurementState(hash, style.height.type.hashCode); - hash = _hashReusableIntrinsicMeasurementState(hash, style.minWidth.type.hashCode); - hash = _hashReusableIntrinsicMeasurementState(hash, style.maxWidth.type.hashCode); - hash = _hashReusableIntrinsicMeasurementState(hash, style.minHeight.type.hashCode); - hash = _hashReusableIntrinsicMeasurementState(hash, style.maxHeight.type.hashCode); + hash = + _hashReusableIntrinsicMeasurementState(hash, style.width.type.hashCode); + hash = _hashReusableIntrinsicMeasurementState( + hash, style.height.type.hashCode); + hash = _hashReusableIntrinsicMeasurementState( + hash, style.minWidth.type.hashCode); + hash = _hashReusableIntrinsicMeasurementState( + hash, style.maxWidth.type.hashCode); + hash = _hashReusableIntrinsicMeasurementState( + hash, style.minHeight.type.hashCode); + hash = _hashReusableIntrinsicMeasurementState( + hash, style.maxHeight.type.hashCode); if (style.width.isNotAuto) { hash = _hashReusableIntrinsicMeasurementState( hash, @@ -2505,25 +2854,29 @@ class RenderFlexLayout extends RenderLayoutBox { if (style.minWidth.isNotAuto) { hash = _hashReusableIntrinsicMeasurementState( hash, - _quantizeReusableIntrinsicMeasurementDouble(style.minWidth.computedValue), + _quantizeReusableIntrinsicMeasurementDouble( + style.minWidth.computedValue), ); } if (!style.maxWidth.isNone) { hash = _hashReusableIntrinsicMeasurementState( hash, - _quantizeReusableIntrinsicMeasurementDouble(style.maxWidth.computedValue), + _quantizeReusableIntrinsicMeasurementDouble( + style.maxWidth.computedValue), ); } if (style.minHeight.isNotAuto) { hash = _hashReusableIntrinsicMeasurementState( hash, - _quantizeReusableIntrinsicMeasurementDouble(style.minHeight.computedValue), + _quantizeReusableIntrinsicMeasurementDouble( + style.minHeight.computedValue), ); } if (!style.maxHeight.isNone) { hash = _hashReusableIntrinsicMeasurementState( hash, - _quantizeReusableIntrinsicMeasurementDouble(style.maxHeight.computedValue), + _quantizeReusableIntrinsicMeasurementDouble( + style.maxHeight.computedValue), ); } if (style.flexBasis != null) { @@ -2532,7 +2885,8 @@ class RenderFlexLayout extends RenderLayoutBox { style.flexBasis!.type.hashCode, ); } - final InlineFormattingContext? ifc = effectiveFlowChild.inlineFormattingContext; + final InlineFormattingContext? ifc = + effectiveFlowChild.inlineFormattingContext; if (ifc != null) { hash = _hashReusableIntrinsicMeasurementState( hash, @@ -2544,11 +2898,9 @@ class RenderFlexLayout extends RenderLayoutBox { _FlexIntrinsicMeasurementLookupResult _lookupReusableIntrinsicMeasurement( RenderBox child, - BoxConstraints childConstraints, - { + BoxConstraints childConstraints, { bool allowAnonymous = false, - } - ) { + }) { if (!allowAnonymous) { return const _FlexIntrinsicMeasurementLookupResult(); } @@ -2559,7 +2911,8 @@ class RenderFlexLayout extends RenderLayoutBox { child, allowAnonymous: allowAnonymous, ); - if (!_isHorizontalFlexDirection || renderStyle.flexWrap != FlexWrap.nowrap) { + if (!_isHorizontalFlexDirection || + renderStyle.flexWrap != FlexWrap.nowrap) { return const _FlexIntrinsicMeasurementLookupResult(); } if (_hasBaselineAlignmentForChild(child)) { @@ -2634,12 +2987,13 @@ class RenderFlexLayout extends RenderLayoutBox { } final _FlexIntrinsicMeasurementCacheBucket bucket = _childrenIntrinsicMeasureCache[child] ?? - _FlexIntrinsicMeasurementCacheBucket(); + _FlexIntrinsicMeasurementCacheBucket(); bucket.store(_FlexIntrinsicMeasurementCacheEntry( constraints: childConstraints, size: Size.copy(childSize), intrinsicMainSize: intrinsicMainSize, - reusableStateSignature: _computeReusableIntrinsicMeasurementStateSignature( + reusableStateSignature: + _computeReusableIntrinsicMeasurementStateSignature( child, childConstraints, flowChild: flowChild, @@ -2656,7 +3010,8 @@ class RenderFlexLayout extends RenderLayoutBox { if (flowChild == null) { return false; } - return child is RenderEventListener || flowChild.renderStyle.isSelfAnonymousFlowLayout(); + return child is RenderEventListener || + flowChild.renderStyle.isSelfAnonymousFlowLayout(); } bool _shouldAvoidParentUsesSizeForFlexChild(RenderBox child) { @@ -2800,8 +3155,8 @@ class RenderFlexLayout extends RenderLayoutBox { } } - if (effectiveRoot - is ContainerRenderObjectMixin>) { + if (effectiveRoot is ContainerRenderObjectMixin>) { RenderBox? child = (effectiveRoot as dynamic).firstChild as RenderBox?; while (child != null) { if (_flowSubtreeContainsReusableTextHeavyContent(child)) { @@ -3008,7 +3363,8 @@ class RenderFlexLayout extends RenderLayoutBox { return true; } - List<_RunMetrics>? _tryBuildEarlyNoFlexNoStretchNoBaselineRunMetrics(List children) { + List<_RunMetrics>? _tryBuildEarlyNoFlexNoStretchNoBaselineRunMetrics( + List children) { if (!_isHorizontalFlexDirection) { _recordEarlyFastPathReject( _FlexFastPathRejectReason.verticalDirection, @@ -3058,7 +3414,8 @@ class RenderFlexLayout extends RenderLayoutBox { _layoutChildForFlex(child, childConstraints); _cacheOriginalConstraintsIfNeeded(child, childConstraints); - final RenderLayoutParentData? childParentData = child.parentData as RenderLayoutParentData?; + final RenderLayoutParentData? childParentData = + child.parentData as RenderLayoutParentData?; childParentData?.runIndex = 0; final double childMainSize = _getMainSize(child); @@ -3068,9 +3425,11 @@ class RenderFlexLayout extends RenderLayoutBox { runMainAxisExtent += mainAxisGap; } runMainAxisExtent += _getMainAxisExtent(child); - runCrossAxisExtent = math.max(runCrossAxisExtent, _getCrossAxisExtent(child)); + runCrossAxisExtent = + math.max(runCrossAxisExtent, _getCrossAxisExtent(child)); - final RenderBoxModel? effectiveChild = child is RenderBoxModel ? child : null; + final RenderBoxModel? effectiveChild = + child is RenderBoxModel ? child : null; final _RunChild runChild = _createRunChildMetadata( child, childMainSize, @@ -3088,7 +3447,8 @@ class RenderFlexLayout extends RenderLayoutBox { } final List<_RunMetrics> runMetrics = <_RunMetrics>[ - _RunMetrics(runMainAxisExtent, runCrossAxisExtent, totalFlexGrow, totalFlexShrink, 0, runChildren, 0) + _RunMetrics(runMainAxisExtent, runCrossAxisExtent, totalFlexGrow, + totalFlexShrink, 0, runChildren, 0) ]; _flexLineBoxMetrics = runMetrics; @@ -3133,11 +3493,14 @@ class RenderFlexLayout extends RenderLayoutBox { // Prepare children of different type for layout. RenderBox? child = firstChild; while (child != null) { - final RenderLayoutParentData childParentData = child.parentData as RenderLayoutParentData; + final RenderLayoutParentData childParentData = + child.parentData as RenderLayoutParentData; if (child is RenderBoxModel && - (child.renderStyle.isSelfPositioned() || child.renderStyle.isSelfStickyPosition())) { + (child.renderStyle.isSelfPositioned() || + child.renderStyle.isSelfStickyPosition())) { positionedChildren.add(child); - } else if (child is RenderPositionPlaceholder && _isPlaceholderPositioned(child)) { + } else if (child is RenderPositionPlaceholder && + _isPlaceholderPositioned(child)) { positionPlaceholderChildren.add(child); } else { flexItemChildren.add(child); @@ -3182,7 +3545,8 @@ class RenderFlexLayout extends RenderLayoutBox { for (RenderBoxModel child in positionedChildren) { final CSSRenderStyle rs = child.renderStyle; final CSSPositionType pos = rs.position; - final bool isAbsOrFixed = pos == CSSPositionType.absolute || pos == CSSPositionType.fixed; + final bool isAbsOrFixed = + pos == CSSPositionType.absolute || pos == CSSPositionType.fixed; final bool hasExplicitMaxHeight = !rs.maxHeight.isNone; final bool hasExplicitMinHeight = !rs.minHeight.isAuto; if (isAbsOrFixed && @@ -3192,7 +3556,8 @@ class RenderFlexLayout extends RenderLayoutBox { rs.bottom.isNotAuto && !hasExplicitMaxHeight && !hasExplicitMinHeight) { - CSSPositionedLayout.layoutPositionedChild(this, child, needsRelayout: true); + CSSPositionedLayout.layoutPositionedChild(this, child, + needsRelayout: true); } } @@ -3201,7 +3566,8 @@ class RenderFlexLayout extends RenderLayoutBox { } // init overflowLayout size - initOverflowLayout(Rect.fromLTRB(0, 0, size.width, size.height), Rect.fromLTRB(0, 0, size.width, size.height)); + initOverflowLayout(Rect.fromLTRB(0, 0, size.width, size.height), + Rect.fromLTRB(0, 0, size.width, size.height)); // calculate all flexItem child overflow size addOverflowLayoutFromChildren(orderedChildren); @@ -3253,9 +3619,11 @@ class RenderFlexLayout extends RenderLayoutBox { return; } - List<_RunMetrics>? runMetrics = _tryBuildEarlyNoFlexNoStretchNoBaselineRunMetrics(children); + List<_RunMetrics>? runMetrics = + _tryBuildEarlyNoFlexNoStretchNoBaselineRunMetrics(children); if (runMetrics != null) { - final bool hasStretchedChildren = _hasStretchedChildrenInCrossAxis(runMetrics); + final bool hasStretchedChildren = + _hasStretchedChildrenInCrossAxis(runMetrics); if (!hasStretchedChildren && _canAttemptFullEarlyFastPath(runMetrics)) { final _FlexResolutionInputs inputs = _computeFlexResolutionInputs(); if (_tryNoFlexNoStretchNoBaselineFastPath( @@ -3290,7 +3658,8 @@ class RenderFlexLayout extends RenderLayoutBox { if (runMetrics == null) { // Layout children to compute metrics of flex lines. if (!kReleaseMode) { - developer.Timeline.startSync('RenderFlex.layoutFlexItems.computeRunMetrics', + developer.Timeline.startSync( + 'RenderFlex.layoutFlexItems.computeRunMetrics', arguments: {'renderObject': describeIdentity(this)}); } @@ -3304,7 +3673,8 @@ class RenderFlexLayout extends RenderLayoutBox { _setContainerSize(runMetrics); if (!kReleaseMode) { - developer.Timeline.startSync('RenderFlex.layoutFlexItems.adjustChildrenSize'); + developer.Timeline.startSync( + 'RenderFlex.layoutFlexItems.adjustChildrenSize'); } // Adjust children size based on flex properties which may affect children size. @@ -3318,7 +3688,8 @@ class RenderFlexLayout extends RenderLayoutBox { } if (!kReleaseMode) { - developer.Timeline.startSync('RenderFlex.layoutFlexItems.setChildrenOffset'); + developer.Timeline.startSync( + 'RenderFlex.layoutFlexItems.setChildrenOffset'); } // Set children offset based on flex alignment properties. @@ -3329,7 +3700,8 @@ class RenderFlexLayout extends RenderLayoutBox { } if (!kReleaseMode) { - developer.Timeline.startSync('RenderFlex.layoutFlexItems.setMaxScrollableSize'); + developer.Timeline.startSync( + 'RenderFlex.layoutFlexItems.setMaxScrollableSize'); } // Set the size of scrollable overflow area for flex layout. @@ -3348,27 +3720,32 @@ class RenderFlexLayout extends RenderLayoutBox { // Cache CSS baselines for this flex container during layout to avoid cross-child baseline computation later. double? containerBaseline; CSSDisplay? effectiveDisplay = renderStyle.effectiveDisplay; - bool isDisplayInline = effectiveDisplay != CSSDisplay.block && effectiveDisplay != CSSDisplay.flex; + bool isDisplayInline = effectiveDisplay != CSSDisplay.block && + effectiveDisplay != CSSDisplay.flex; double? getChildBaselineDistance(RenderBox child) { // Prefer WebF's cached CSS baselines, which are safe to access even when the // render tree is not attached to a PipelineOwner (e.g. offscreen/manual layout). if (child is RenderBoxModel) { - final double? css = child.computeCssFirstBaselineOf(TextBaseline.alphabetic); + final double? css = + child.computeCssFirstBaselineOf(TextBaseline.alphabetic); if (css != null) return css; } else if (child is RenderPositionPlaceholder) { final RenderBoxModel? positioned = child.positioned; - final double? css = positioned?.computeCssFirstBaselineOf(TextBaseline.alphabetic); + final double? css = + positioned?.computeCssFirstBaselineOf(TextBaseline.alphabetic); if (css != null) return css; } // Avoid RenderBox.getDistanceToBaseline when detached; it asserts on owner!. if (!child.attached) { if (child is RenderBoxModel) { - return child.boxSize?.height ?? (child.hasSize ? child.size.height : null); + return child.boxSize?.height ?? + (child.hasSize ? child.size.height : null); } if (child is RenderPositionPlaceholder) { - return child.boxSize?.height ?? (child.hasSize ? child.size.height : null); + return child.boxSize?.height ?? + (child.hasSize ? child.size.height : null); } return child.hasSize ? child.size.height : null; } @@ -3406,9 +3783,11 @@ class RenderFlexLayout extends RenderLayoutBox { // baseline-aligned item no longer anchors the container baseline in IFC. if (_isChildCrossAxisMarginAutoExist(candidate)) return false; final AlignSelf self = _getAlignSelf(candidate); - if (self == AlignSelf.baseline || self == AlignSelf.lastBaseline) return true; + if (self == AlignSelf.baseline || self == AlignSelf.lastBaseline) + return true; if (self == AlignSelf.auto && - (renderStyle.alignItems == AlignItems.baseline || renderStyle.alignItems == AlignItems.lastBaseline)) { + (renderStyle.alignItems == AlignItems.baseline || + renderStyle.alignItems == AlignItems.lastBaseline)) { return true; } return false; @@ -3452,7 +3831,8 @@ class RenderFlexLayout extends RenderLayoutBox { // For inline flex containers, include bottom margin to synthesize an // external baseline from the bottom margin edge. if (isDisplayInline) { - containerBaseline = borderBoxHeight + renderStyle.marginBottom.computedValue; + containerBaseline = + borderBoxHeight + renderStyle.marginBottom.computedValue; } else { containerBaseline = borderBoxHeight; } @@ -3470,15 +3850,18 @@ class RenderFlexLayout extends RenderLayoutBox { final List<_RunChild> firstRunChildren = firstLineMetrics.runChildren; if (firstRunChildren.isNotEmpty) { RenderBox? baselineChild; - double? baselineDistance; // distance from child's border-top to its baseline + double? + baselineDistance; // distance from child's border-top to its baseline RenderBox? fallbackChild; double? fallbackBaseline; bool participatesInBaseline(RenderBox candidate) { final AlignSelf self = _getAlignSelf(candidate); - if (self == AlignSelf.baseline || self == AlignSelf.lastBaseline) return true; + if (self == AlignSelf.baseline || self == AlignSelf.lastBaseline) + return true; if (self == AlignSelf.auto && - (renderStyle.alignItems == AlignItems.baseline || renderStyle.alignItems == AlignItems.lastBaseline)) { + (renderStyle.alignItems == AlignItems.baseline || + renderStyle.alignItems == AlignItems.lastBaseline)) { return true; } return false; @@ -3512,7 +3895,8 @@ class RenderFlexLayout extends RenderLayoutBox { (self == AlignSelf.baseline || self == AlignSelf.lastBaseline || (self == AlignSelf.auto && - (renderStyle.alignItems == AlignItems.baseline || renderStyle.alignItems == AlignItems.lastBaseline))); + (renderStyle.alignItems == AlignItems.baseline || + renderStyle.alignItems == AlignItems.lastBaseline))); double dy = 0; if (child.parentData is RenderLayoutParentData) { dy = (child.parentData as RenderLayoutParentData).offset.dy; @@ -3537,7 +3921,8 @@ class RenderFlexLayout extends RenderLayoutBox { // Prefer the first baseline-participating child (excluding cross-axis auto margins); // otherwise fall back to the first child that exposes a baseline; otherwise the first item. final RenderBox? chosen = baselineChild ?? fallbackChild; - final double? chosenBaseline = baselineChild != null ? baselineDistance : fallbackBaseline; + final double? chosenBaseline = + baselineChild != null ? baselineDistance : fallbackBaseline; if (chosen != null) { if (chosenBaseline != null) { @@ -3549,7 +3934,8 @@ class RenderFlexLayout extends RenderLayoutBox { dy = pd.offset.dy; } if (chosen is RenderBoxModel) { - final Offset? rel = CSSPositionedLayout.getRelativeOffset(chosen.renderStyle); + final Offset? rel = + CSSPositionedLayout.getRelativeOffset(chosen.renderStyle); if (rel != null) dy -= rel.dy; } containerBaseline = chosenBaseline + dy; @@ -3558,7 +3944,8 @@ class RenderFlexLayout extends RenderLayoutBox { final double borderBoxHeight = boxSize?.height ?? size.height; // If inline-level (inline-flex), synthesize from bottom margin edge. if (isDisplayInline) { - containerBaseline = borderBoxHeight + renderStyle.marginBottom.computedValue; + containerBaseline = + borderBoxHeight + renderStyle.marginBottom.computedValue; } else { containerBaseline = borderBoxHeight; } @@ -3574,7 +3961,8 @@ class RenderFlexLayout extends RenderLayoutBox { List positionPlaceholderChildren = [child]; // Layout children to compute metrics of flex lines. - List<_RunMetrics> runMetrics = _computeRunMetrics(positionPlaceholderChildren); + List<_RunMetrics> runMetrics = + _computeRunMetrics(positionPlaceholderChildren); // Set children offset based on flex alignment properties. _setChildrenOffset(runMetrics); @@ -3582,12 +3970,15 @@ class RenderFlexLayout extends RenderLayoutBox { // Layout children in normal flow order to calculate metrics of flex lines according to its constraints // and flex-wrap property. - List<_RunMetrics> _computeRunMetrics(List children,) { + List<_RunMetrics> _computeRunMetrics( + List children, + ) { List<_RunMetrics> runMetrics = <_RunMetrics>[]; if (children.isEmpty) return runMetrics; final bool isHorizontal = _isHorizontalFlexDirection; - final bool isWrap = renderStyle.flexWrap == FlexWrap.wrap || renderStyle.flexWrap == FlexWrap.wrapReverse; + final bool isWrap = renderStyle.flexWrap == FlexWrap.wrap || + renderStyle.flexWrap == FlexWrap.wrapReverse; final double mainAxisGap = _getMainAxisGap(); double runMainAxisExtent = 0.0; @@ -3624,9 +4015,8 @@ class RenderFlexLayout extends RenderLayoutBox { for (int childIndex = 0; childIndex < children.length; childIndex++) { final RenderBox child = children[childIndex]; final BoxConstraints childConstraints = _getIntrinsicConstraints(child); - final bool isMetricsOnlyMeasureChild = - allowAnonymousMetricsOnlyCache && - _isMetricsOnlyIntrinsicMeasureChild(child); + final bool isMetricsOnlyMeasureChild = allowAnonymousMetricsOnlyCache && + _isMetricsOnlyIntrinsicMeasureChild(child); final _FlexIntrinsicMeasurementLookupResult cacheLookup = _lookupReusableIntrinsicMeasurement( child, @@ -3688,24 +4078,27 @@ class RenderFlexLayout extends RenderLayoutBox { double? candidate; if (flowChild.inlineFormattingContext != null) { // Paragraph max-intrinsic width approximates the max-content contribution. - final double paraMax = flowChild.inlineFormattingContext!.paragraphMaxIntrinsicWidth; + final double paraMax = flowChild + .inlineFormattingContext!.paragraphMaxIntrinsicWidth; // Convert content-width to border-box width by adding horizontal padding + borders. - final double paddingBorderH = - cs.paddingLeft.computedValue + - cs.paddingRight.computedValue + - cs.effectiveBorderLeftWidth.computedValue + - cs.effectiveBorderRightWidth.computedValue; + final double paddingBorderH = cs.paddingLeft.computedValue + + cs.paddingRight.computedValue + + cs.effectiveBorderLeftWidth.computedValue + + cs.effectiveBorderRightWidth.computedValue; candidate = (paraMax.isFinite ? paraMax : 0) + paddingBorderH; } else { // Fallback: use max intrinsic width (already includes padding/border). - final double maxIntrinsic = flowChild.getMaxIntrinsicWidth(double.infinity); + final double maxIntrinsic = + flowChild.getMaxIntrinsicWidth(double.infinity); if (maxIntrinsic.isFinite) { candidate = maxIntrinsic; } } // If the currently measured intrinsic width is larger (e.g., filled to container), // prefer the content-based candidate to avoid unintended expansion. - if (candidate != null && candidate > 0 && candidate < intrinsicMain) { + if (candidate != null && + candidate > 0 && + candidate < intrinsicMain) { intrinsicMain = candidate; } } @@ -3729,10 +4122,14 @@ class RenderFlexLayout extends RenderLayoutBox { // intrinsicMain is the border-box main size. In WebF (border-box model), // min-width/max-width are already specified for the border box. Do not // add padding/border again when clamping. - if (maxMain != null && maxMain.isFinite && intrinsicMain > maxMain) { + if (maxMain != null && + maxMain.isFinite && + intrinsicMain > maxMain) { intrinsicMain = maxMain; } - if (minMain != null && minMain.isFinite && intrinsicMain < minMain) { + if (minMain != null && + minMain.isFinite && + intrinsicMain < minMain) { intrinsicMain = minMain; } } @@ -3743,18 +4140,21 @@ class RenderFlexLayout extends RenderLayoutBox { bool hasPctMaxMain = isHorizontal ? child.renderStyle.maxWidth.type == CSSLengthType.PERCENTAGE : child.renderStyle.maxHeight.type == CSSLengthType.PERCENTAGE; - bool hasAutoMain = isHorizontal ? child.renderStyle.width.isAuto : child.renderStyle.height - .isAuto; + bool hasAutoMain = isHorizontal + ? child.renderStyle.width.isAuto + : child.renderStyle.height.isAuto; if (hasPctMaxMain && hasAutoMain) { double paddingBorderMain = isHorizontal ? (child.renderStyle.effectiveBorderLeftWidth.computedValue + - child.renderStyle.effectiveBorderRightWidth.computedValue + - child.renderStyle.paddingLeft.computedValue + - child.renderStyle.paddingRight.computedValue) + child + .renderStyle.effectiveBorderRightWidth.computedValue + + child.renderStyle.paddingLeft.computedValue + + child.renderStyle.paddingRight.computedValue) : (child.renderStyle.effectiveBorderTopWidth.computedValue + - child.renderStyle.effectiveBorderBottomWidth.computedValue + - child.renderStyle.paddingTop.computedValue + - child.renderStyle.paddingBottom.computedValue); + child.renderStyle.effectiveBorderBottomWidth + .computedValue + + child.renderStyle.paddingTop.computedValue + + child.renderStyle.paddingBottom.computedValue); // Check if this is an empty element (no content) using DOM-based detection bool isEmptyElement = false; @@ -3777,24 +4177,28 @@ class RenderFlexLayout extends RenderLayoutBox { } } - _storeIntrinsicMeasurementCache(child, childConstraints, childSize, intrinsicMain); + _storeIntrinsicMeasurementCache( + child, childConstraints, childSize, intrinsicMain); } - final RenderLayoutParentData? childParentData = child.parentData as RenderLayoutParentData?; + final RenderLayoutParentData? childParentData = + child.parentData as RenderLayoutParentData?; _childrenIntrinsicMainSizes[child] = intrinsicMain; - Size? intrinsicChildSize = _getChildSize(child, shouldUseIntrinsicMainSize: true); + Size? intrinsicChildSize = + _getChildSize(child, shouldUseIntrinsicMainSize: true); - double childMainAxisExtent = _getMainAxisExtent(child, shouldUseIntrinsicMainSize: true); + double childMainAxisExtent = + _getMainAxisExtent(child, shouldUseIntrinsicMainSize: true); double childCrossAxisExtent = _getCrossAxisExtent(child); // Include gap spacing in flex line limit check double gapSpacing = runChildren.isNotEmpty ? mainAxisGap : 0; - bool isExceedFlexLineLimit = runMainAxisExtent + gapSpacing + childMainAxisExtent > flexLineLimit; + bool isExceedFlexLineLimit = + runMainAxisExtent + gapSpacing + childMainAxisExtent > + flexLineLimit; // calculate flex line - if (isWrap && - runChildren.isNotEmpty && - isExceedFlexLineLimit) { + if (isWrap && runChildren.isNotEmpty && isExceedFlexLineLimit) { runMetrics.add(_RunMetrics( runMainAxisExtent, runCrossAxisExtent, @@ -3822,8 +4226,7 @@ class RenderFlexLayout extends RenderLayoutBox { // Vertical align is only valid for inline box. // Baseline alignment in column direction behave the same as flex-start. AlignSelf alignSelf = _getAlignSelf(child); - bool isBaselineAlign = - alignSelf == AlignSelf.baseline || + bool isBaselineAlign = alignSelf == AlignSelf.baseline || alignSelf == AlignSelf.lastBaseline || renderStyle.alignItems == AlignItems.baseline || renderStyle.alignItems == AlignItems.lastBaseline; @@ -3849,7 +4252,10 @@ class RenderFlexLayout extends RenderLayoutBox { maxSizeAboveBaseline, ); maxSizeBelowBaseline = math.max( - childMarginTop + childMarginBottom + intrinsicChildSize!.height - childAscent, + childMarginTop + + childMarginBottom + + intrinsicChildSize!.height - + childAscent, maxSizeBelowBaseline, ); runCrossAxisExtent = maxSizeAboveBaseline + maxSizeBelowBaseline; @@ -3860,7 +4266,8 @@ class RenderFlexLayout extends RenderLayoutBox { 'runCross=${runCrossAxisExtent.toStringAsFixed(2)}'); } } else { - runCrossAxisExtent = math.max(runCrossAxisExtent, childCrossAxisExtent); + runCrossAxisExtent = + math.max(runCrossAxisExtent, childCrossAxisExtent); } // Per CSS Flexbox §9.7, keep two sizes: @@ -3870,8 +4277,10 @@ class RenderFlexLayout extends RenderLayoutBox { // We store the base size in runChild.originalMainSize so remaining free // space and shrink/grow weighting use the correct base, and keep the // clamped value in _childrenIntrinsicMainSizes for line metrics. - final RenderBoxModel? effectiveChild = child is RenderBoxModel ? child : null; - final double? usedFlexBasis = effectiveChild != null ? _getUsedFlexBasis(child) : null; + final RenderBoxModel? effectiveChild = + child is RenderBoxModel ? child : null; + final double? usedFlexBasis = + effectiveChild != null ? _getUsedFlexBasis(child) : null; double baseMainSize; if (usedFlexBasis != null) { @@ -3880,7 +4289,9 @@ class RenderFlexLayout extends RenderLayoutBox { // items share free space evenly regardless of padding/border, while the // non-flex portion (padding/border) is accounted for separately in totalSpace. final CSSLengthValue? fb = effectiveChild?.renderStyle.flexBasis; - if (fb != null && fb.type == CSSLengthType.PERCENTAGE && fb.computedValue == 0) { + if (fb != null && + fb.type == CSSLengthType.PERCENTAGE && + fb.computedValue == 0) { baseMainSize = 0; } else { baseMainSize = usedFlexBasis; @@ -3937,10 +4348,14 @@ class RenderFlexLayout extends RenderLayoutBox { for (RenderBox child in children) { RenderBoxModel? box = child is RenderBoxModel ? child - : (child is RenderEventListener ? child.child as RenderBoxModel? : null); + : (child is RenderEventListener + ? child.child as RenderBoxModel? + : null); if (box != null) { - bool hasPercentageMaxWidth = box.renderStyle.maxWidth.type == CSSLengthType.PERCENTAGE; - bool hasPercentageMaxHeight = box.renderStyle.maxHeight.type == CSSLengthType.PERCENTAGE; + bool hasPercentageMaxWidth = + box.renderStyle.maxWidth.type == CSSLengthType.PERCENTAGE; + bool hasPercentageMaxHeight = + box.renderStyle.maxHeight.type == CSSLengthType.PERCENTAGE; if (hasPercentageMaxWidth || hasPercentageMaxHeight) { // Store the final constraints for use in _adjustChildrenSize @@ -3954,7 +4369,9 @@ class RenderFlexLayout extends RenderLayoutBox { } // Compute the leading and between spacing of each flex line. - _RunSpacing _computeRunSpacing(List<_RunMetrics> runMetrics,) { + _RunSpacing _computeRunSpacing( + List<_RunMetrics> runMetrics, + ) { double? contentBoxLogicalWidth = renderStyle.contentBoxLogicalWidth; double? contentBoxLogicalHeight = renderStyle.contentBoxLogicalHeight; double containerCrossAxisExtent = 0.0; @@ -3974,7 +4391,8 @@ class RenderFlexLayout extends RenderLayoutBox { double runBetweenSpace = 0.0; // Align-content only works in when flex-wrap is no nowrap. - if (renderStyle.flexWrap == FlexWrap.wrap || renderStyle.flexWrap == FlexWrap.wrapReverse) { + if (renderStyle.flexWrap == FlexWrap.wrap || + renderStyle.flexWrap == FlexWrap.wrapReverse) { switch (renderStyle.alignContent) { case AlignContent.flexStart: case AlignContent.start: @@ -3990,7 +4408,8 @@ class RenderFlexLayout extends RenderLayoutBox { if (crossAxisFreeSpace < 0) { runBetweenSpace = 0; } else { - runBetweenSpace = runCount > 1 ? crossAxisFreeSpace / (runCount - 1) : 0.0; + runBetweenSpace = + runCount > 1 ? crossAxisFreeSpace / (runCount - 1) : 0.0; } break; case AlignContent.spaceAround: @@ -4032,7 +4451,9 @@ class RenderFlexLayout extends RenderLayoutBox { // Find the size in the cross axis of flex lines. // @TODO: add cache to avoid recalculate in one layout stage. - double _getRunsCrossSize(List<_RunMetrics> runMetrics,) { + double _getRunsCrossSize( + List<_RunMetrics> runMetrics, + ) { double crossSize = 0; double crossAxisGap = _getCrossAxisGap(); for (int i = 0; i < runMetrics.length; i++) { @@ -4047,9 +4468,12 @@ class RenderFlexLayout extends RenderLayoutBox { // Find the max size in the main axis of flex lines. // @TODO: add cache to avoid recalculate in one layout stage. - double _getRunsMaxMainSize(List<_RunMetrics> runMetrics,) { + double _getRunsMaxMainSize( + List<_RunMetrics> runMetrics, + ) { // Find the max size of flex lines. - _RunMetrics maxMainSizeMetrics = runMetrics.reduce((_RunMetrics curr, _RunMetrics next) { + _RunMetrics maxMainSizeMetrics = + runMetrics.reduce((_RunMetrics curr, _RunMetrics next) { return curr.mainAxisExtent > next.mainAxisExtent ? curr : next; }); return maxMainSizeMetrics.mainAxisExtent; @@ -4057,7 +4481,11 @@ class RenderFlexLayout extends RenderLayoutBox { // Resolve flex item length if flex-grow or flex-shrink exists. // https://www.w3.org/TR/css-flexbox-1/#resolve-flexible-lengths - bool _resolveFlexibleLengths(_RunMetrics runMetric, _FlexFactorTotals totalFlexFactor, double initialFreeSpace,) { + bool _resolveFlexibleLengths( + _RunMetrics runMetric, + _FlexFactorTotals totalFlexFactor, + double initialFreeSpace, + ) { final List<_RunChild> runChildren = runMetric.runChildren; final double totalFlexGrow = totalFlexFactor.flexGrow; final double totalFlexShrink = totalFlexFactor.flexShrink; @@ -4066,7 +4494,8 @@ class RenderFlexLayout extends RenderLayoutBox { bool isFlexGrow = initialFreeSpace > 0 && totalFlexGrow > 0; bool isFlexShrink = initialFreeSpace < 0 && totalFlexShrink > 0; - double sumFlexFactors = isFlexGrow ? totalFlexGrow : (isFlexShrink ? totalFlexShrink : 0); + double sumFlexFactors = + isFlexGrow ? totalFlexGrow : (isFlexShrink ? totalFlexShrink : 0); // Per CSS Flexbox §9.7, if the sum of the unfrozen flex items’ flex // factors is less than 1, multiply the free space by this sum. @@ -4099,7 +4528,8 @@ class RenderFlexLayout extends RenderLayoutBox { final double childFlexShrink = runChild.flexShrink; if (childFlexShrink == 0) continue; - final double baseSize = (runChild.usedFlexBasis ?? runChild.originalMainSize); + final double baseSize = + (runChild.usedFlexBasis ?? runChild.originalMainSize); totalWeightedFlexShrink += baseSize * childFlexShrink; } } @@ -4109,7 +4539,8 @@ class RenderFlexLayout extends RenderLayoutBox { if (runChild.frozen) continue; // Use used flex-basis (border-box) for originalMainSize when definite. - final double originalMainSize = runChild.usedFlexBasis ?? runChild.originalMainSize; + final double originalMainSize = + runChild.usedFlexBasis ?? runChild.originalMainSize; double computedSize = originalMainSize; double flexedMainSize = originalMainSize; @@ -4119,7 +4550,8 @@ class RenderFlexLayout extends RenderLayoutBox { // Re-evaluate grow/shrink based on current remaining free space sign. final bool doGrow = spacePerFlex != null && flexGrow > 0; - final bool doShrink = remainingFreeSpace < 0 && totalFlexShrink > 0 && flexShrink > 0; + final bool doShrink = + remainingFreeSpace < 0 && totalFlexShrink > 0 && flexShrink > 0; if (doGrow) { computedSize = originalMainSize + spacePerFlex * flexGrow; @@ -4130,7 +4562,8 @@ class RenderFlexLayout extends RenderLayoutBox { // distribution itself. if (totalWeightedFlexShrink != 0) { final double scaledShrink = originalMainSize * flexShrink; - computedSize = originalMainSize + (scaledShrink / totalWeightedFlexShrink) * remainingFreeSpace; + computedSize = originalMainSize + + (scaledShrink / totalWeightedFlexShrink) * remainingFreeSpace; } } @@ -4150,15 +4583,20 @@ class RenderFlexLayout extends RenderLayoutBox { if (clampTarget != null) { double minMainAxisSize = _getMinMainAxisSize(clampTarget); double maxMainAxisSize = _getMaxMainAxisSize(clampTarget); - if (computedSize < minMainAxisSize && (computedSize - minMainAxisSize).abs() >= minFlexPrecision) { + if (computedSize < minMainAxisSize && + (computedSize - minMainAxisSize).abs() >= minFlexPrecision) { flexedMainSize = minMainAxisSize; - } else if (computedSize > maxMainAxisSize && (computedSize - maxMainAxisSize).abs() >= minFlexPrecision) { + } else if (computedSize > maxMainAxisSize && + (computedSize - maxMainAxisSize).abs() >= minFlexPrecision) { flexedMainSize = maxMainAxisSize; } } } - double violation = (flexedMainSize - computedSize).abs() >= minFlexPrecision ? flexedMainSize - computedSize : 0; + double violation = + (flexedMainSize - computedSize).abs() >= minFlexPrecision + ? flexedMainSize - computedSize + : 0; // Collect all the flex items with violations. if (violation > 0) { @@ -4177,7 +4615,8 @@ class RenderFlexLayout extends RenderLayoutBox { runChild.frozen = true; } } else { - List<_RunChild> violations = totalViolation < 0 ? maxViolations : minViolations; + List<_RunChild> violations = + totalViolation < 0 ? maxViolations : minViolations; // Find all the violations, set main size and freeze all the flex items. for (int i = 0; i < violations.length; i++) { @@ -4187,7 +4626,8 @@ class RenderFlexLayout extends RenderLayoutBox { // item in this iteration (relative to its original size). If the // item was clamped to its min/max, flexedMainSize equals the clamp // result; its delta reflects how much free space it actually took. - runMetric.remainingFreeSpace -= runChild.flexedMainSize - runChild.originalMainSize; + runMetric.remainingFreeSpace -= + runChild.flexedMainSize - runChild.originalMainSize; double flexGrow = runChild.flexGrow; double flexShrink = runChild.flexShrink; @@ -4240,10 +4680,19 @@ class RenderFlexLayout extends RenderLayoutBox { _FlexFastPathRejectCallback? onReject, }) { final bool isHorizontal = _isHorizontalFlexDirection; + final bool adjustProfilerEnabled = _FlexAdjustFastPathProfiler.enabled; + final String? adjustProfilerPath = + adjustProfilerEnabled ? _describeFastPathContainer() : null; final double mainAxisGap = _getMainAxisGap(); final double containerStyleMin = isHorizontal - ? (renderStyle.minWidth.isNotAuto ? renderStyle.minWidth.computedValue : 0.0) - : (renderStyle.minHeight.isNotAuto ? renderStyle.minHeight.computedValue : 0.0); + ? (renderStyle.minWidth.isNotAuto + ? renderStyle.minWidth.computedValue + : 0.0) + : (renderStyle.minHeight.isNotAuto + ? renderStyle.minHeight.computedValue + : 0.0); + int relayoutRowCount = 0; + int relayoutChildCount = 0; // First, verify no run will actually enter flexible length resolution. for (final _RunMetrics metrics in runMetrics) { @@ -4251,7 +4700,8 @@ class RenderFlexLayout extends RenderLayoutBox { double totalSpace = 0; for (final _RunChild runChild in runChildrenList) { - final double childSpace = runChild.usedFlexBasis ?? runChild.originalMainSize; + final double childSpace = + runChild.usedFlexBasis ?? runChild.originalMainSize; totalSpace += childSpace + runChild.mainAxisMargin; } @@ -4260,7 +4710,8 @@ class RenderFlexLayout extends RenderLayoutBox { totalSpace += (itemCount - 1) * mainAxisGap; } - final double freeSpace = maxMainSize != null ? (maxMainSize - totalSpace) : 0.0; + final double freeSpace = + maxMainSize != null ? (maxMainSize - totalSpace) : 0.0; final bool boundedOnly = maxMainSize != null && !(isHorizontal @@ -4271,7 +4722,8 @@ class RenderFlexLayout extends RenderLayoutBox { (contentConstraints?.hasTightHeight ?? false) || constraints.hasTightHeight)); - final bool willShrink = maxMainSize != null && freeSpace < 0 && metrics.totalFlexShrink > 0; + final bool willShrink = + maxMainSize != null && freeSpace < 0 && metrics.totalFlexShrink > 0; bool willGrow = false; if (metrics.totalFlexGrow > 0 && !boundedOnly) { @@ -4285,6 +4737,18 @@ class RenderFlexLayout extends RenderLayoutBox { } if (willShrink) { + if (adjustProfilerEnabled && adjustProfilerPath != null) { + _FlexAdjustFastPathProfiler.recordReject( + adjustProfilerPath, + _FlexAdjustFastPathRejectReason.wouldShrink, + details: { + 'freeSpace': freeSpace.toStringAsFixed(2), + 'totalSpace': totalSpace.toStringAsFixed(2), + 'maxMainSize': maxMainSize?.toStringAsFixed(2), + 'totalFlexShrink': metrics.totalFlexShrink.toStringAsFixed(2), + }, + ); + } onReject?.call( _FlexFastPathRejectReason.wouldShrink, details: { @@ -4297,6 +4761,21 @@ class RenderFlexLayout extends RenderLayoutBox { return false; } if (willGrow) { + if (adjustProfilerEnabled && adjustProfilerPath != null) { + _FlexAdjustFastPathProfiler.recordReject( + adjustProfilerPath, + _FlexAdjustFastPathRejectReason.wouldGrow, + details: { + 'freeSpace': freeSpace.toStringAsFixed(2), + 'totalSpace': totalSpace.toStringAsFixed(2), + 'maxMainSize': maxMainSize?.toStringAsFixed(2), + 'totalFlexGrow': metrics.totalFlexGrow.toStringAsFixed(2), + 'boundedOnly': boundedOnly, + 'isMainSizeDefinite': isMainSizeDefinite, + 'containerStyleMin': containerStyleMin.toStringAsFixed(2), + }, + ); + } onReject?.call( _FlexFastPathRejectReason.wouldGrow, details: { @@ -4327,20 +4806,38 @@ class RenderFlexLayout extends RenderLayoutBox { final double childOldMainSize = _getMainSize(child); final double? desiredPreservedMain = _childrenIntrinsicMainSizes[child]; + _FlexAdjustFastPathRelayoutReason? relayoutReason; - bool needsLayout = effectiveChild.needsRelayout || - (_childrenRequirePostMeasureLayout[child] == true); - if (!needsLayout && desiredPreservedMain != null && desiredPreservedMain != childOldMainSize) { + bool needsLayout = false; + if (effectiveChild.needsRelayout) { + needsLayout = true; + relayoutReason = + _FlexAdjustFastPathRelayoutReason.effectiveChildNeedsRelayout; + } else if (_childrenRequirePostMeasureLayout[child] == true) { needsLayout = true; + relayoutReason = _FlexAdjustFastPathRelayoutReason.postMeasureLayout; + } else if (desiredPreservedMain != null && + desiredPreservedMain != childOldMainSize) { + needsLayout = true; + relayoutReason = + _FlexAdjustFastPathRelayoutReason.preservedMainMismatch; } if (!needsLayout && desiredPreservedMain != null) { final BoxConstraints applied = child.constraints; final bool autoMain = isHorizontal ? effectiveChild.renderStyle.width.isAuto : effectiveChild.renderStyle.height.isAuto; - final bool wasNonTightMain = isHorizontal ? !applied.hasTightWidth : !applied.hasTightHeight; + final bool wasNonTightMain = + isHorizontal ? !applied.hasTightWidth : !applied.hasTightHeight; if (autoMain && wasNonTightMain) { - needsLayout = true; + final bool preservedMainMatches = + (desiredPreservedMain - childOldMainSize).abs() < 0.5; + final bool textOnlySubtree = _hasOnlyTextFlexRelayoutSubtree(child); + if (!preservedMainMatches || !textOnlySubtree) { + needsLayout = true; + relayoutReason = _FlexAdjustFastPathRelayoutReason + .autoMainWithNonTightConstraint; + } } } @@ -4351,11 +4848,38 @@ class RenderFlexLayout extends RenderLayoutBox { final double measuredBorderW = _getChildSize(effectiveChild)!.width; if (measuredBorderW > availCross + 0.5) { needsLayout = true; + relayoutReason = + _FlexAdjustFastPathRelayoutReason.columnAutoCrossOverflow; } } } if (!needsLayout) continue; + relayoutReason ??= + _FlexAdjustFastPathRelayoutReason.effectiveChildNeedsRelayout; + if (adjustProfilerEnabled && adjustProfilerPath != null) { + final Map details = { + 'oldMain': childOldMainSize.toStringAsFixed(2), + 'desiredMain': desiredPreservedMain?.toStringAsFixed(2), + 'autoMain': isHorizontal + ? effectiveChild.renderStyle.width.isAuto + : effectiveChild.renderStyle.height.isAuto, + 'appliedMainTight': isHorizontal + ? child.constraints.hasTightWidth + : child.constraints.hasTightHeight, + }; + if (!isHorizontal && availCross.isFinite) { + details['availCross'] = availCross.toStringAsFixed(2); + } + _FlexAdjustFastPathProfiler.recordRelayout( + adjustProfilerPath, + _describeFastPathChild(child), + relayoutReason, + childConstraints: child.constraints, + details: details, + ); + } + relayoutChildCount++; _markFlexRelayoutForTextOnly(effectiveChild); @@ -4371,6 +4895,7 @@ class RenderFlexLayout extends RenderLayoutBox { } if (!didRelayout) continue; + relayoutRowCount++; // Recompute run extents from the final child sizes. double mainAxisExtent = 0; @@ -4385,20 +4910,30 @@ class RenderFlexLayout extends RenderLayoutBox { metrics.crossAxisExtent = crossAxisExtent; } + if (adjustProfilerEnabled && adjustProfilerPath != null) { + _FlexAdjustFastPathProfiler.recordHit( + adjustProfilerPath, + relayoutRowCount: relayoutRowCount, + relayoutChildCount: relayoutChildCount, + ); + } return true; } // Adjust children size (not include position placeholder) based on // flex factors (flex-grow/flex-shrink) and alignment in cross axis (align-items). // https://www.w3.org/TR/css-flexbox-1/#resolve-flexible-lengths - void _adjustChildrenSize(List<_RunMetrics> runMetrics,) { + void _adjustChildrenSize( + List<_RunMetrics> runMetrics, + ) { if (runMetrics.isEmpty) return; final bool isHorizontal = _isHorizontalFlexDirection; final double mainAxisGap = _getMainAxisGap(); final bool hasBaselineAlignment = _hasBaselineAlignedChildren(runMetrics); final bool canAttemptFastPath = isHorizontal && !hasBaselineAlignment; - final bool hasStretchedChildren = - canAttemptFastPath ? _hasStretchedChildrenInCrossAxis(runMetrics) : true; + final bool hasStretchedChildren = canAttemptFastPath + ? _hasStretchedChildrenInCrossAxis(runMetrics) + : true; final _FlexResolutionInputs inputs = _computeFlexResolutionInputs(); final double? contentBoxLogicalWidth = inputs.contentBoxLogicalWidth; final double? contentBoxLogicalHeight = inputs.contentBoxLogicalHeight; @@ -4430,7 +4965,8 @@ class RenderFlexLayout extends RenderLayoutBox { double totalSpace = 0; // Flex factor calculation depends on flex-basis if exists. for (final _RunChild runChild in runChildrenList) { - final double childSpace = runChild.usedFlexBasis ?? runChild.originalMainSize; + final double childSpace = + runChild.usedFlexBasis ?? runChild.originalMainSize; totalSpace += childSpace + runChild.mainAxisMargin; } @@ -4445,9 +4981,14 @@ class RenderFlexLayout extends RenderLayoutBox { // For definite main sizes (tight or specified) or auto main size bounded by a max constraint, // distribute free space per spec. Positive free space allows grow when flex-basis is 0 (e.g., flex: 1), // negative free space triggers shrink when items overflow. - final bool boundedOnly = maxMainSize != null && !(isHorizontal - ? (contentBoxLogicalWidth != null || (contentConstraints?.hasTightWidth ?? false) || constraints.hasTightWidth) - : (contentBoxLogicalHeight != null || (contentConstraints?.hasTightHeight ?? false) || constraints.hasTightHeight)); + final bool boundedOnly = maxMainSize != null && + !(isHorizontal + ? (contentBoxLogicalWidth != null || + (contentConstraints?.hasTightWidth ?? false) || + constraints.hasTightWidth) + : (contentBoxLogicalHeight != null || + (contentConstraints?.hasTightHeight ?? false) || + constraints.hasTightHeight)); double initialFreeSpace = 0; if (maxMainSize != null) { initialFreeSpace = maxMainSize - totalSpace; @@ -4465,16 +5006,19 @@ class RenderFlexLayout extends RenderLayoutBox { // Only honor a definite author-specified min-main-size on the container. double containerStyleMin = 0.0; if (isHorizontal) { - if (renderStyle.minWidth.isNotAuto) containerStyleMin = renderStyle.minWidth.computedValue; + if (renderStyle.minWidth.isNotAuto) + containerStyleMin = renderStyle.minWidth.computedValue; } else { - if (renderStyle.minHeight.isNotAuto) containerStyleMin = renderStyle.minHeight.computedValue; + if (renderStyle.minHeight.isNotAuto) + containerStyleMin = renderStyle.minHeight.computedValue; } // If a definite min is set, treat that as the available main size headroom. // Otherwise, keep the container's main size content-driven with zero // distributable positive free space. if (containerStyleMin > 0) { - final double inferredMain = math.max(containerStyleMin, layoutContentMainSize); + final double inferredMain = + math.max(containerStyleMin, layoutContentMainSize); maxMainSize = inferredMain; initialFreeSpace = inferredMain - totalSpace; } else { @@ -4484,15 +5028,18 @@ class RenderFlexLayout extends RenderLayoutBox { } } - double layoutContentMainSize = isHorizontal ? contentSize.width : contentSize.height; + double layoutContentMainSize = + isHorizontal ? contentSize.width : contentSize.height; // Only consider an author-specified (definite) min-main-size on the flex container here. // Do not use the automatic min size, which includes padding/border, to synthesize // positive free space; that incorrectly inflates the container (e.g., to 360). double containerStyleMin = 0.0; if (isHorizontal) { - if (renderStyle.minWidth.isNotAuto) containerStyleMin = renderStyle.minWidth.computedValue; + if (renderStyle.minWidth.isNotAuto) + containerStyleMin = renderStyle.minWidth.computedValue; } else { - if (renderStyle.minHeight.isNotAuto) containerStyleMin = renderStyle.minHeight.computedValue; + if (renderStyle.minHeight.isNotAuto) + containerStyleMin = renderStyle.minHeight.computedValue; } // Adapt free space only when the container has a definite CSS min-main-size. if (initialFreeSpace == 0) { @@ -4502,12 +5049,18 @@ class RenderFlexLayout extends RenderLayoutBox { if (maxMainSize < minTarget) { maxMainSize = minTarget; - double maxMainConstraints = - _isHorizontalFlexDirection ? contentConstraints!.maxWidth : contentConstraints!.maxHeight; + double maxMainConstraints = _isHorizontalFlexDirection + ? contentConstraints!.maxWidth + : contentConstraints!.maxHeight; // determining isScrollingContentBox is to reduce the scope of influence - if (renderStyle.isSelfScrollingContainer() && maxMainConstraints.isFinite) { - maxMainSize = totalFlexShrink > 0 ? math.min(maxMainSize, maxMainConstraints) : maxMainSize; - maxMainSize = totalFlexGrow > 0 ? math.max(maxMainSize, maxMainConstraints) : maxMainSize; + if (renderStyle.isSelfScrollingContainer() && + maxMainConstraints.isFinite) { + maxMainSize = totalFlexShrink > 0 + ? math.min(maxMainSize, maxMainConstraints) + : maxMainSize; + maxMainSize = totalFlexGrow > 0 + ? math.max(maxMainSize, maxMainConstraints) + : maxMainSize; } initialFreeSpace = maxMainSize - totalSpace; @@ -4530,9 +5083,11 @@ class RenderFlexLayout extends RenderLayoutBox { // a definite CSS min-main-size exists on the flex container itself. double containerStyleMin = 0.0; if (isHorizontal) { - if (renderStyle.minWidth.isNotAuto) containerStyleMin = renderStyle.minWidth.computedValue; + if (renderStyle.minWidth.isNotAuto) + containerStyleMin = renderStyle.minWidth.computedValue; } else { - if (renderStyle.minHeight.isNotAuto) containerStyleMin = renderStyle.minHeight.computedValue; + if (renderStyle.minHeight.isNotAuto) + containerStyleMin = renderStyle.minHeight.computedValue; } if (containerStyleMin <= 0 || containerStyleMin <= totalSpace) { @@ -4562,7 +5117,8 @@ class RenderFlexLayout extends RenderLayoutBox { flexShrink: metrics.totalFlexShrink, ); // Loop flex items to resolve flexible length of flex items with flex factor. - while (_resolveFlexibleLengths(metrics, totalFlexFactor, usedFreeSpace)) {} + while ( + _resolveFlexibleLengths(metrics, totalFlexFactor, usedFreeSpace)) {} } // Phase 1 — Relayout each item with its resolved main size only. @@ -4580,7 +5136,8 @@ class RenderFlexLayout extends RenderLayoutBox { // Determine used main size from the flexible lengths result, if any. double? childFlexedMainSize; - if ((isFlexGrow && runChild.flexGrow > 0) || (isFlexShrink && runChild.flexShrink > 0)) { + if ((isFlexGrow && runChild.flexGrow > 0) || + (isFlexShrink && runChild.flexShrink > 0)) { childFlexedMainSize = runChild.flexedMainSize; } @@ -4592,7 +5149,9 @@ class RenderFlexLayout extends RenderLayoutBox { bool needsLayout = (childFlexedMainSize != null) || (effectiveChild.needsRelayout) || (_childrenRequirePostMeasureLayout[child] == true); - if (!needsLayout && desiredPreservedMain != null && (desiredPreservedMain != childOldMainSize)) { + if (!needsLayout && + desiredPreservedMain != null && + (desiredPreservedMain != childOldMainSize)) { needsLayout = true; } if (!needsLayout && desiredPreservedMain != null) { @@ -4616,7 +5175,8 @@ class RenderFlexLayout extends RenderLayoutBox { if (!needsLayout && !_isHorizontalFlexDirection) { final bool childCrossAuto = effectiveChild.renderStyle.width.isAuto; final bool noStretch = !_needToStretchChildCrossSize(effectiveChild); - final double availCross = contentConstraints?.maxWidth ?? double.infinity; + final double availCross = + contentConstraints?.maxWidth ?? double.infinity; if (childCrossAuto && noStretch && availCross.isFinite) { // Compare against the border-box width measured during intrinsic pass. final double measuredBorderW = _getChildSize(effectiveChild)!.width; @@ -4653,9 +5213,10 @@ class RenderFlexLayout extends RenderLayoutBox { double? childStretchedCrossSize; if (_needToStretchChildCrossSize(effectiveChild)) { - childStretchedCrossSize = - _getChildStretchedCrossSize(effectiveChild, metrics.crossAxisExtent, runBetweenSpace); - if (effectiveChild is RenderLayoutBox && effectiveChild.isNegativeMarginChangeHSize) { + childStretchedCrossSize = _getChildStretchedCrossSize( + effectiveChild, metrics.crossAxisExtent, runBetweenSpace); + if (effectiveChild is RenderLayoutBox && + effectiveChild.isNegativeMarginChangeHSize) { double childCrossAxisMargin = _isHorizontalFlexDirection ? effectiveChild.renderStyle.margin.vertical : effectiveChild.renderStyle.margin.horizontal; @@ -4712,8 +5273,10 @@ class RenderFlexLayout extends RenderLayoutBox { double childCrossMargin = 0; if (child is RenderBoxModel) { childCrossMargin = isHorizontal - ? child.renderStyle.marginTop.computedValue + child.renderStyle.marginBottom.computedValue - : child.renderStyle.marginLeft.computedValue + child.renderStyle.marginRight.computedValue; + ? child.renderStyle.marginTop.computedValue + + child.renderStyle.marginBottom.computedValue + : child.renderStyle.marginLeft.computedValue + + child.renderStyle.marginRight.computedValue; } double childCrossExtent = childCrossSize + childCrossMargin; @@ -4733,8 +5296,7 @@ class RenderFlexLayout extends RenderLayoutBox { // Vertical align is only valid for inline box. // Baseline alignment in column direction behave the same as flex-start. AlignSelf alignSelf = _getAlignSelf(child); - bool isBaselineAlign = - alignSelf == AlignSelf.baseline || + bool isBaselineAlign = alignSelf == AlignSelf.baseline || alignSelf == AlignSelf.lastBaseline || renderStyle.alignItems == AlignItems.baseline || renderStyle.alignItems == AlignItems.lastBaseline; @@ -4767,7 +5329,8 @@ class RenderFlexLayout extends RenderLayoutBox { // Get constraints of flex items which needs to change size due to // flex-grow/flex-shrink or align-items stretch. - BoxConstraints _getChildAdjustedConstraints(RenderBoxModel child, + BoxConstraints _getChildAdjustedConstraints( + RenderBoxModel child, double? childFlexedMainSize, double? childStretchedCrossSize, int lineChildrenCount, @@ -4791,12 +5354,15 @@ class RenderFlexLayout extends RenderLayoutBox { } } - if (child.renderStyle.isSelfRenderReplaced() && child.renderStyle.aspectRatio != null) { - _overrideReplacedChildLength(child, childFlexedMainSize, childStretchedCrossSize); + if (child.renderStyle.isSelfRenderReplaced() && + child.renderStyle.aspectRatio != null) { + _overrideReplacedChildLength( + child, childFlexedMainSize, childStretchedCrossSize); } // Use stored percentage constraints if available, otherwise use current constraints - BoxConstraints oldConstraints = _childrenOldConstraints[child] ?? child.constraints; + BoxConstraints oldConstraints = + _childrenOldConstraints[child] ?? child.constraints; // Row flex container + pure cross-axis stretch: // Preserve the flex-resolved main size (oldConstraints.min/maxWidth) and @@ -4835,8 +5401,9 @@ class RenderFlexLayout extends RenderLayoutBox { if (contentW != null && contentW.isFinite) { final double pad = child.renderStyle.paddingLeft.computedValue + child.renderStyle.paddingRight.computedValue; - final double border = child.renderStyle.effectiveBorderLeftWidth.computedValue + - child.renderStyle.effectiveBorderRightWidth.computedValue; + final double border = + child.renderStyle.effectiveBorderLeftWidth.computedValue + + child.renderStyle.effectiveBorderRightWidth.computedValue; return math.max(0, contentW + pad + border); } return math.max(0, oldConstraints.maxWidth); @@ -4849,8 +5416,9 @@ class RenderFlexLayout extends RenderLayoutBox { if (contentH != null && contentH.isFinite) { final double pad = child.renderStyle.paddingTop.computedValue + child.renderStyle.paddingBottom.computedValue; - final double border = child.renderStyle.effectiveBorderTopWidth.computedValue + - child.renderStyle.effectiveBorderBottomWidth.computedValue; + final double border = + child.renderStyle.effectiveBorderTopWidth.computedValue + + child.renderStyle.effectiveBorderBottomWidth.computedValue; return math.max(0, contentH + pad + border); } return math.max(0, oldConstraints.maxHeight); @@ -4865,10 +5433,14 @@ class RenderFlexLayout extends RenderLayoutBox { double minConstraintWidth = child.hasOverrideContentLogicalWidth ? safeUsedBorderBoxWidth() - : (oldConstraints.minWidth > maxConstraintWidth ? maxConstraintWidth : oldConstraints.minWidth); + : (oldConstraints.minWidth > maxConstraintWidth + ? maxConstraintWidth + : oldConstraints.minWidth); double minConstraintHeight = child.hasOverrideContentLogicalHeight ? safeUsedBorderBoxHeight() - : (oldConstraints.minHeight > maxConstraintHeight ? maxConstraintHeight : oldConstraints.minHeight); + : (oldConstraints.minHeight > maxConstraintHeight + ? maxConstraintHeight + : oldConstraints.minHeight); // If the flex item has a definite height in a row-direction container, // lock the child's height to the used border-box height. This prevents @@ -4925,12 +5497,16 @@ class RenderFlexLayout extends RenderLayoutBox { // overflowing. This mirrors practical browser behavior for scrollable widgets when // author intent is flex: 1; min-height: 0 under a max-height container. if (!_isHorizontalFlexDirection) { - final bool containerBounded = (contentConstraints?.hasBoundedHeight ?? false) && - (contentConstraints?.maxHeight.isFinite ?? false); + final bool containerBounded = + (contentConstraints?.hasBoundedHeight ?? false) && + (contentConstraints?.maxHeight.isFinite ?? false); if (containerBounded) { final double cap = contentConstraints!.maxHeight; - final bool childIsWidget = child is RenderWidget || child.renderStyle.target is WidgetElement; - if (childIsWidget && preserveMainAxisSize != null && preserveMainAxisSize > cap) { + final bool childIsWidget = + child is RenderWidget || child.renderStyle.target is WidgetElement; + if (childIsWidget && + preserveMainAxisSize != null && + preserveMainAxisSize > cap) { minConstraintHeight = cap; maxConstraintHeight = cap; } @@ -4944,9 +5520,12 @@ class RenderFlexLayout extends RenderLayoutBox { // Do not apply this optimization to replaced elements; their aspect-ratio handling // and intrinsic sizing already provide stable behavior, and locking can produce // intermediate widths that conflict with later stretch. - if (!_isHorizontalFlexDirection && childStretchedCrossSize == null && !child.renderStyle.isSelfRenderReplaced()) { + if (!_isHorizontalFlexDirection && + childStretchedCrossSize == null && + !child.renderStyle.isSelfRenderReplaced()) { final bool childCrossAuto = child.renderStyle.width.isAuto; - final bool childCrossPercent = child.renderStyle.width.type == CSSLengthType.PERCENTAGE; + final bool childCrossPercent = + child.renderStyle.width.type == CSSLengthType.PERCENTAGE; // Determine the effective cross-axis alignment for this child. final AlignSelf selfAlign = _getAlignSelf(child); @@ -4960,7 +5539,8 @@ class RenderFlexLayout extends RenderLayoutBox { // column-direction flex items with non-stretch alignment. if (childCrossAuto && !isStretchAlignment) { final WhiteSpace ws = child.renderStyle.whiteSpace; - final bool allowOverflowCross = ws == WhiteSpace.pre || ws == WhiteSpace.nowrap; + final bool allowOverflowCross = + ws == WhiteSpace.pre || ws == WhiteSpace.nowrap; if (allowOverflowCross) { // For unbreakable text (`pre`/`nowrap`), let the item overflow the flex container in // the cross axis by giving it an unbounded width constraint. This allows the span @@ -4972,15 +5552,18 @@ class RenderFlexLayout extends RenderLayoutBox { // Compute shrink-to-fit width in the cross axis for column flex items: // used = min(max(min-content, available), max-content). // Work in content-box, then convert to border-box by adding padding+border. - final double paddingBorderH = child.renderStyle.padding.horizontal + child.renderStyle.border.horizontal; + final double paddingBorderH = child.renderStyle.padding.horizontal + + child.renderStyle.border.horizontal; // Min-content contribution (content-box). final double minContentCB = child.minContentWidth; // Max-content contribution (content-box). Prefer IFC paragraph max-intrinsic width for flow content. double maxContentCB = minContentCB; // fallback - if (child is RenderFlowLayout && child.inlineFormattingContext != null) { - final double paraMax = child.inlineFormattingContext!.paragraphMaxIntrinsicWidth; + if (child is RenderFlowLayout && + child.inlineFormattingContext != null) { + final double paraMax = + child.inlineFormattingContext!.paragraphMaxIntrinsicWidth; if (paraMax.isFinite && paraMax > 0) maxContentCB = paraMax; } @@ -4990,20 +5573,25 @@ class RenderFlexLayout extends RenderLayoutBox { // cross axis. Subtract positive start/end margins to get the space // available to the item’s border-box for shrink-to-fit width. double availableCross = double.infinity; - if (contentConstraints != null && contentConstraints!.maxWidth.isFinite) { + if (contentConstraints != null && + contentConstraints!.maxWidth.isFinite) { availableCross = contentConstraints!.maxWidth; } else { // Fallback to current laid-out content width when known. - final double borderH = renderStyle.effectiveBorderLeftWidth.computedValue + - renderStyle.effectiveBorderRightWidth.computedValue; + final double borderH = + renderStyle.effectiveBorderLeftWidth.computedValue + + renderStyle.effectiveBorderRightWidth.computedValue; final double fallback = math.max(0.0, size.width - borderH); if (fallback.isFinite && fallback > 0) availableCross = fallback; } // Subtract cross-axis margins (positive values only) from available width. if (availableCross.isFinite) { - final double startMargin = _flowAwareChildCrossAxisMargin(child) ?? 0; - final double endMargin = _flowAwareChildCrossAxisMargin(child, isEnd: true) ?? 0; - final double marginDeduction = math.max(0.0, startMargin) + math.max(0.0, endMargin); + final double startMargin = + _flowAwareChildCrossAxisMargin(child) ?? 0; + final double endMargin = + _flowAwareChildCrossAxisMargin(child, isEnd: true) ?? 0; + final double marginDeduction = + math.max(0.0, startMargin) + math.max(0.0, endMargin); availableCross = math.max(0.0, availableCross - marginDeduction); } @@ -5013,8 +5601,11 @@ class RenderFlexLayout extends RenderLayoutBox { // regressing centering cases (e.g., column-wrap with align-self:center). if (maxContentCB <= minContentCB + 0.5) { final double priorBorderW = _getChildSize(child)!.width; - final double priorContentW = - math.max(0.0, priorBorderW - (child.renderStyle.padding.horizontal + child.renderStyle.border.horizontal)); + final double priorContentW = math.max( + 0.0, + priorBorderW - + (child.renderStyle.padding.horizontal + + child.renderStyle.border.horizontal)); if (priorContentW.isFinite && priorContentW > minContentCB) { maxContentCB = priorContentW; } @@ -5022,12 +5613,15 @@ class RenderFlexLayout extends RenderLayoutBox { // Convert to border-box for comparison with constraints we apply to the child. final double minBorder = math.max(0.0, minContentCB + paddingBorderH); - final double maxBorder = math.max(minBorder, maxContentCB + paddingBorderH); - final double availBorder = availableCross.isFinite ? availableCross : double.infinity; + final double maxBorder = + math.max(minBorder, maxContentCB + paddingBorderH); + final double availBorder = + availableCross.isFinite ? availableCross : double.infinity; double shrinkBorderW = maxBorder; if (availBorder.isFinite) { - shrinkBorderW = math.min(math.max(minBorder, availBorder), maxBorder); + shrinkBorderW = + math.min(math.max(minBorder, availBorder), maxBorder); } // Respect the child’s own min/max-width caps. @@ -5036,21 +5630,25 @@ class RenderFlexLayout extends RenderLayoutBox { if (child.renderStyle.minWidth.isNotAuto) { if (child.renderStyle.minWidth.type == CSSLengthType.PERCENTAGE) { if (availableCross.isFinite) { - final double usedMin = (child.renderStyle.minWidth.value ?? 0) * availableCross; + final double usedMin = + (child.renderStyle.minWidth.value ?? 0) * availableCross; shrinkBorderW = math.max(shrinkBorderW, usedMin); } } else { - shrinkBorderW = math.max(shrinkBorderW, child.renderStyle.minWidth.computedValue); + shrinkBorderW = math.max( + shrinkBorderW, child.renderStyle.minWidth.computedValue); } } if (child.renderStyle.maxWidth.isNotNone) { if (child.renderStyle.maxWidth.type == CSSLengthType.PERCENTAGE) { if (availableCross.isFinite) { - final double usedMax = (child.renderStyle.maxWidth.value ?? 0) * availableCross; + final double usedMax = + (child.renderStyle.maxWidth.value ?? 0) * availableCross; shrinkBorderW = math.min(shrinkBorderW, usedMax); } } else { - shrinkBorderW = math.min(shrinkBorderW, child.renderStyle.maxWidth.computedValue); + shrinkBorderW = math.min( + shrinkBorderW, child.renderStyle.maxWidth.computedValue); } } @@ -5065,17 +5663,24 @@ class RenderFlexLayout extends RenderLayoutBox { // (or when alignment falls back to stretch). This preserves previous layout for // stretch cases and avoids regressions. double fixedW; - if (child.renderStyle.isSelfRenderReplaced() && child.renderStyle.aspectRatio != null) { - final double usedBorderBoxH = child.renderStyle.borderBoxLogicalHeight ?? _getChildSize(child)!.height; + if (child.renderStyle.isSelfRenderReplaced() && + child.renderStyle.aspectRatio != null) { + final double usedBorderBoxH = + child.renderStyle.borderBoxLogicalHeight ?? + _getChildSize(child)!.height; fixedW = usedBorderBoxH * child.renderStyle.aspectRatio!; } else { fixedW = _getChildSize(child)!.width; } - final double containerCrossMax = contentConstraints?.maxWidth ?? double.infinity; + final double containerCrossMax = + contentConstraints?.maxWidth ?? double.infinity; final double containerContentW = containerCrossMax.isFinite ? containerCrossMax - : math.max(0.0, size.width - (renderStyle.effectiveBorderLeftWidth.computedValue + - renderStyle.effectiveBorderRightWidth.computedValue)); + : math.max( + 0.0, + size.width - + (renderStyle.effectiveBorderLeftWidth.computedValue + + renderStyle.effectiveBorderRightWidth.computedValue)); double styleMinW = 0.0; final CSSLengthValue minWLen = child.renderStyle.minWidth; @@ -5093,7 +5698,8 @@ class RenderFlexLayout extends RenderLayoutBox { } fixedW = fixedW.clamp(styleMinW, styleMaxW); - if (containerContentW.isFinite) fixedW = math.min(fixedW, containerContentW); + if (containerContentW.isFinite) + fixedW = math.min(fixedW, containerContentW); if (fixedW.isFinite && fixedW > 0) { minConstraintWidth = fixedW; @@ -5105,22 +5711,29 @@ class RenderFlexLayout extends RenderLayoutBox { // once it becomes known (second layout pass). This matches CSS flex item percentage resolution // in column-direction containers. double containerContentW; - if (contentConstraints != null && contentConstraints!.maxWidth.isFinite) { + if (contentConstraints != null && + contentConstraints!.maxWidth.isFinite) { containerContentW = contentConstraints!.maxWidth; } else { - final double borderH = renderStyle.effectiveBorderLeftWidth.computedValue + - renderStyle.effectiveBorderRightWidth.computedValue; + final double borderH = + renderStyle.effectiveBorderLeftWidth.computedValue + + renderStyle.effectiveBorderRightWidth.computedValue; containerContentW = math.max(0, size.width - borderH); } final double percent = child.renderStyle.width.value ?? 0; - double childBorderBoxW = containerContentW.isFinite ? (containerContentW * percent) : 0; - if (!childBorderBoxW.isFinite || childBorderBoxW < 0) childBorderBoxW = 0; - - if (child.renderStyle.maxWidth.isNotNone && child.renderStyle.maxWidth.type != CSSLengthType.PERCENTAGE) { - childBorderBoxW = math.min(childBorderBoxW, child.renderStyle.maxWidth.computedValue); + double childBorderBoxW = + containerContentW.isFinite ? (containerContentW * percent) : 0; + if (!childBorderBoxW.isFinite || childBorderBoxW < 0) + childBorderBoxW = 0; + + if (child.renderStyle.maxWidth.isNotNone && + child.renderStyle.maxWidth.type != CSSLengthType.PERCENTAGE) { + childBorderBoxW = math.min( + childBorderBoxW, child.renderStyle.maxWidth.computedValue); } if (child.renderStyle.minWidth.isNotAuto) { - childBorderBoxW = math.max(childBorderBoxW, child.renderStyle.minWidth.computedValue); + childBorderBoxW = math.max( + childBorderBoxW, child.renderStyle.minWidth.computedValue); } minConstraintWidth = childBorderBoxW; @@ -5132,10 +5745,14 @@ class RenderFlexLayout extends RenderLayoutBox { // For replaced elements in a column flex container during phase 1 (no cross stretch yet), // cap the available cross size by the container’s content-box width so the intrinsic sizing // does not expand to unconstrained widths (e.g., viewport width) before the stretch phase. - if (!_isHorizontalFlexDirection && child.renderStyle.isSelfRenderReplaced() && childStretchedCrossSize == null) { - final double containerCrossMax = contentConstraints?.maxWidth ?? double.infinity; + if (!_isHorizontalFlexDirection && + child.renderStyle.isSelfRenderReplaced() && + childStretchedCrossSize == null) { + final double containerCrossMax = + contentConstraints?.maxWidth ?? double.infinity; if (containerCrossMax.isFinite) { - if (maxConstraintWidth.isInfinite || maxConstraintWidth > containerCrossMax) { + if (maxConstraintWidth.isInfinite || + maxConstraintWidth > containerCrossMax) { maxConstraintWidth = containerCrossMax; } if (minConstraintWidth > maxConstraintWidth) { @@ -5147,19 +5764,22 @@ class RenderFlexLayout extends RenderLayoutBox { // For elements or any inline-level elements in horizontal flex layout, // avoid tight height constraints during secondary layout passes. // This allows text to properly reflow and adjust its height when width changes. - bool isTextElement = child.renderStyle.isSelfRenderWidget() && child.renderStyle.target is WebFTextElement; - bool isInlineElementWithText = (child.renderStyle.display == CSSDisplay.inline || - child.renderStyle.display == CSSDisplay.inlineBlock || - child.renderStyle.display == CSSDisplay.inlineFlex) && - (child.renderStyle.isSelfRenderFlowLayout() || child.renderStyle.isSelfRenderFlexLayout()); - bool isAutoHeightLayoutContainer = - child.renderStyle.height.isAuto && + bool isTextElement = child.renderStyle.isSelfRenderWidget() && + child.renderStyle.target is WebFTextElement; + bool isInlineElementWithText = + (child.renderStyle.display == CSSDisplay.inline || + child.renderStyle.display == CSSDisplay.inlineBlock || + child.renderStyle.display == CSSDisplay.inlineFlex) && + (child.renderStyle.isSelfRenderFlowLayout() || + child.renderStyle.isSelfRenderFlexLayout()); + bool isAutoHeightLayoutContainer = child.renderStyle.height.isAuto && (child.renderStyle.isSelfRenderFlowLayout() || child.renderStyle.isSelfRenderFlexLayout()); // Block-level flex items whose contents form an inline formatting context (e.g., a
with only text) // also need height to be unconstrained on the secondary pass so text can wrap after flex-shrink. // This mirrors browser behavior: first resolve the used main size, then measure cross size with auto height. - bool establishesIFC = child.renderStyle.shouldEstablishInlineFormattingContext(); + bool establishesIFC = + child.renderStyle.shouldEstablishInlineFormattingContext(); bool isSecondaryLayoutPass = child.hasSize; // Allow dynamic height adjustment during secondary layout when width has changed and height is auto @@ -5171,7 +5791,8 @@ class RenderFlexLayout extends RenderLayoutBox { isAutoHeightLayoutContainer) && // For non-flexed items, only allow when this is the only item on the line // so the line cross-size is content-driven. - (childFlexedMainSize != null || (preserveMainAxisSize != null && lineChildrenCount == 1)) && + (childFlexedMainSize != null || + (preserveMainAxisSize != null && lineChildrenCount == 1)) && // Layout containers with auto height need a chance to grow past the // previous stretched cross size when their own contents change. (childStretchedCrossSize == null || @@ -5185,7 +5806,8 @@ class RenderFlexLayout extends RenderLayoutBox { // margins and padding are preserved while still allowing it to expand beyond // the stretched size if its contents require it. if (childStretchedCrossSize != null && childStretchedCrossSize > 0) { - minConstraintHeight = math.max(minConstraintHeight, childStretchedCrossSize); + minConstraintHeight = + math.max(minConstraintHeight, childStretchedCrossSize); } else { minConstraintHeight = 0; } @@ -5205,8 +5827,10 @@ class RenderFlexLayout extends RenderLayoutBox { if (!child.renderStyle.paddingBottom.isAuto) { contentMinHeight += child.renderStyle.paddingBottom.computedValue; } - contentMinHeight += child.renderStyle.effectiveBorderTopWidth.computedValue; - contentMinHeight += child.renderStyle.effectiveBorderBottomWidth.computedValue; + contentMinHeight += + child.renderStyle.effectiveBorderTopWidth.computedValue; + contentMinHeight += + child.renderStyle.effectiveBorderBottomWidth.computedValue; // Allow child to expand beyond parent's maxHeight if content requires it // This matches browser behavior where content can overflow constrained parents @@ -5250,19 +5874,23 @@ class RenderFlexLayout extends RenderLayoutBox { final bool isReplaced = child.renderStyle.isSelfRenderReplaced(); final double childFlexGrow = _getFlexGrow(child); final double childFlexShrink = _getFlexShrink(child); - final bool isFlexNone = childFlexGrow == 0 && childFlexShrink == 0; // flex: none - if (hasDefiniteFlexBasis || (child.renderStyle.width.isAuto && !isReplaced)) { + final bool isFlexNone = + childFlexGrow == 0 && childFlexShrink == 0; // flex: none + if (hasDefiniteFlexBasis || + (child.renderStyle.width.isAuto && !isReplaced)) { if (isFlexNone) { // For flex: none items, do not constrain to the container width. // Let the item keep its preserved (intrinsic) width and overflow if necessary. minConstraintWidth = preserveMainAxisSize; maxConstraintWidth = preserveMainAxisSize; } else { - final double containerAvail = contentConstraints?.maxWidth ?? double.infinity; + final double containerAvail = + contentConstraints?.maxWidth ?? double.infinity; if (containerAvail.isFinite) { double cap = preserveMainAxisSize; // Also honor the child’s own definite (non-percentage) max-width if any. - if (child.renderStyle.maxWidth.isNotNone && child.renderStyle.maxWidth.type != CSSLengthType.PERCENTAGE) { + if (child.renderStyle.maxWidth.isNotNone && + child.renderStyle.maxWidth.type != CSSLengthType.PERCENTAGE) { cap = math.min(cap, child.renderStyle.maxWidth.computedValue); } cap = math.min(cap, containerAvail); @@ -5285,9 +5913,10 @@ class RenderFlexLayout extends RenderLayoutBox { // container must not force a tight height on the item; allow the child to // reflow under the container's bounded height instead of freezing to the // intrinsic height from PASS 2. - final bool containerBoundedOnly = (contentConstraints?.hasBoundedHeight ?? false) && - !(contentConstraints?.hasTightHeight ?? false) && - renderStyle.contentBoxLogicalHeight == null; + final bool containerBoundedOnly = + (contentConstraints?.hasBoundedHeight ?? false) && + !(contentConstraints?.hasTightHeight ?? false) && + renderStyle.contentBoxLogicalHeight == null; // Avoid over-constraining text reflow cases by applying only when the // intrinsic pass forced a tight zero height or when the basis is definite @@ -5322,9 +5951,11 @@ class RenderFlexLayout extends RenderLayoutBox { // When replaced element is stretched or shrinked only on one axis and // length is not specified on the other axis, the length needs to be // overrided in the other axis. - void _overrideReplacedChildLength(RenderBoxModel child, - double? childFlexedMainSize, - double? childStretchedCrossSize,) { + void _overrideReplacedChildLength( + RenderBoxModel child, + double? childFlexedMainSize, + double? childStretchedCrossSize, + ) { assert(child.renderStyle.isSelfRenderReplaced()); if (childFlexedMainSize != null && childStretchedCrossSize == null) { if (_isHorizontalFlexDirection) { @@ -5344,38 +5975,48 @@ class RenderFlexLayout extends RenderLayoutBox { } // Override replaced child height when its height is auto. - void _overrideReplacedChildHeight(RenderBoxModel child,) { + void _overrideReplacedChildHeight( + RenderBoxModel child, + ) { assert(child.renderStyle.isSelfRenderReplaced()); if (child.renderStyle.height.isAuto) { double maxConstraintWidth = child.renderStyle.borderBoxLogicalWidth!; - double maxConstraintHeight = maxConstraintWidth / child.renderStyle.aspectRatio!; + double maxConstraintHeight = + maxConstraintWidth / child.renderStyle.aspectRatio!; // Clamp replaced element height by min/max height. if (child.renderStyle.minHeight.isNotAuto) { double minHeight = child.renderStyle.minHeight.computedValue; - maxConstraintHeight = maxConstraintHeight < minHeight ? minHeight : maxConstraintHeight; + maxConstraintHeight = + maxConstraintHeight < minHeight ? minHeight : maxConstraintHeight; } if (child.renderStyle.maxHeight.isNotNone) { double maxHeight = child.renderStyle.maxHeight.computedValue; - maxConstraintHeight = maxConstraintHeight > maxHeight ? maxHeight : maxConstraintHeight; + maxConstraintHeight = + maxConstraintHeight > maxHeight ? maxHeight : maxConstraintHeight; } _overrideChildContentBoxLogicalHeight(child, maxConstraintHeight); } } // Override replaced child width when its width is auto. - void _overrideReplacedChildWidth(RenderBoxModel child,) { + void _overrideReplacedChildWidth( + RenderBoxModel child, + ) { assert(child.renderStyle.isSelfRenderReplaced()); if (child.renderStyle.width.isAuto) { double maxConstraintHeight = child.renderStyle.borderBoxLogicalHeight!; - double maxConstraintWidth = maxConstraintHeight * child.renderStyle.aspectRatio!; + double maxConstraintWidth = + maxConstraintHeight * child.renderStyle.aspectRatio!; // Clamp replaced element width by min/max width. if (child.renderStyle.minWidth.isNotAuto) { double minWidth = child.renderStyle.minWidth.computedValue; - maxConstraintWidth = maxConstraintWidth < minWidth ? minWidth : maxConstraintWidth; + maxConstraintWidth = + maxConstraintWidth < minWidth ? minWidth : maxConstraintWidth; } if (child.renderStyle.maxWidth.isNotNone) { double maxWidth = child.renderStyle.maxWidth.computedValue; - maxConstraintWidth = maxConstraintWidth > maxWidth ? maxWidth : maxConstraintWidth; + maxConstraintWidth = + maxConstraintWidth > maxWidth ? maxWidth : maxConstraintWidth; } _overrideChildContentBoxLogicalWidth(child, maxConstraintWidth); } @@ -5383,13 +6024,15 @@ class RenderFlexLayout extends RenderLayoutBox { // Override content box logical width of child when flex-grow/flex-shrink/align-items has changed // child's size. - void _overrideChildContentBoxLogicalWidth(RenderBoxModel child, double maxConstraintWidth) { + void _overrideChildContentBoxLogicalWidth( + RenderBoxModel child, double maxConstraintWidth) { // Deflating padding/border can yield a negative content-box when the // assigned border-box is smaller than padding+border. CSS forbids // negative content sizes; clamp to zero so downstream layout (e.g., // intrinsic measurement and alignment) receives a sane, non-negative // logical size. - double deflated = child.renderStyle.deflatePaddingBorderWidth(maxConstraintWidth); + double deflated = + child.renderStyle.deflatePaddingBorderWidth(maxConstraintWidth); if (deflated.isFinite && deflated < 0) deflated = 0; child.renderStyle.contentBoxLogicalWidth = deflated; child.hasOverrideContentLogicalWidth = true; @@ -5397,18 +6040,22 @@ class RenderFlexLayout extends RenderLayoutBox { // Override content box logical height of child when flex-grow/flex-shrink/align-items has changed // child's size. - void _overrideChildContentBoxLogicalHeight(RenderBoxModel child, double maxConstraintHeight) { + void _overrideChildContentBoxLogicalHeight( + RenderBoxModel child, double maxConstraintHeight) { // See width counterpart: guard against negative content-box heights // when padding/border exceed the assigned border-box. Use zero to // represent the collapsed content box per spec. - double deflated = child.renderStyle.deflatePaddingBorderHeight(maxConstraintHeight); + double deflated = + child.renderStyle.deflatePaddingBorderHeight(maxConstraintHeight); if (deflated.isFinite && deflated < 0) deflated = 0; child.renderStyle.contentBoxLogicalHeight = deflated; child.hasOverrideContentLogicalHeight = true; } // Set flex container size according to children size. - void _setContainerSize(List<_RunMetrics> runMetrics,) { + void _setContainerSize( + List<_RunMetrics> runMetrics, + ) { if (runMetrics.isEmpty) { _setContainerSizeWithNoChild(); return; @@ -5419,13 +6066,16 @@ class RenderFlexLayout extends RenderLayoutBox { // mainAxis gaps are already included in metrics.mainAxisExtent after PASS 3. // No need to add them again as this would double-count and cause incorrect sizing. - double contentWidth = _isHorizontalFlexDirection ? runMaxMainSize : runCrossSize; - double contentHeight = _isHorizontalFlexDirection ? runCrossSize : runMaxMainSize; + double contentWidth = + _isHorizontalFlexDirection ? runMaxMainSize : runCrossSize; + double contentHeight = + _isHorizontalFlexDirection ? runCrossSize : runMaxMainSize; // Respect specified cross size (height for row, width for column) without growing the container. // This allows flex items to overflow when their content is taller/wider than the container. if (_isHorizontalFlexDirection) { - final double? specifiedContentHeight = renderStyle.contentBoxLogicalHeight; + final double? specifiedContentHeight = + renderStyle.contentBoxLogicalHeight; if (specifiedContentHeight != null) { contentHeight = specifiedContentHeight; } @@ -5478,8 +6128,9 @@ class RenderFlexLayout extends RenderLayoutBox { double childMarginBottom = child.renderStyle.marginBottom.computedValue; double childMarginLeft = child.renderStyle.marginLeft.computedValue; double childMarginRight = child.renderStyle.marginRight.computedValue; - runChildMainSize += - _isHorizontalFlexDirection ? childMarginLeft + childMarginRight : childMarginTop + childMarginBottom; + runChildMainSize += _isHorizontalFlexDirection + ? childMarginLeft + childMarginRight + : childMarginTop + childMarginBottom; } runMainExtent += runChildMainSize; } @@ -5488,7 +6139,9 @@ class RenderFlexLayout extends RenderLayoutBox { // Get auto min size in the main axis which equals the main axis size of its contents. // https://www.w3.org/TR/css-sizing-3/#automatic-minimum-size - double _getMainAxisAutoSize(List<_RunMetrics> runMetrics,) { + double _getMainAxisAutoSize( + List<_RunMetrics> runMetrics, + ) { double autoMinSize = 0; // Main size of each run. @@ -5512,7 +6165,8 @@ class RenderFlexLayout extends RenderLayoutBox { for (final _RunChild runChild in runChildren) { final RenderBox child = runChild.child; final Size childSize = _getChildSize(child)!; - final double runChildCrossSize = _isHorizontalFlexDirection ? childSize.height : childSize.width; + final double runChildCrossSize = + _isHorizontalFlexDirection ? childSize.height : childSize.width; runCrossExtent = math.max(runCrossExtent, runChildCrossSize); } runCrossSize.add(runCrossExtent); @@ -5520,7 +6174,9 @@ class RenderFlexLayout extends RenderLayoutBox { // Get auto min size in the cross axis which equals the cross axis size of its contents. // https://www.w3.org/TR/css-sizing-3/#automatic-minimum-size - double _getCrossAxisAutoSize(List<_RunMetrics> runMetrics,) { + double _getCrossAxisAutoSize( + List<_RunMetrics> runMetrics, + ) { double autoMinSize = 0; // Cross size of each run. @@ -5575,7 +6231,8 @@ class RenderFlexLayout extends RenderLayoutBox { final CSSOverflowType overflowX = childRenderStyle.effectiveOverflowX; final CSSOverflowType overflowY = childRenderStyle.effectiveOverflowY; // Only non scroll container need to use scrollable size, otherwise use its own size. - if (overflowX == CSSOverflowType.visible && overflowY == CSSOverflowType.visible) { + if (overflowX == CSSOverflowType.visible && + overflowY == CSSOverflowType.visible) { childScrollableSize = child.scrollableSize; } @@ -5584,33 +6241,42 @@ class RenderFlexLayout extends RenderLayoutBox { // https://www.w3.org/TR/css-overflow-3/#scrollable-overflow-region // Add offset of margin. - childOffsetX += childRenderStyle.marginLeft.computedValue + childRenderStyle.marginRight.computedValue; - childOffsetY += childRenderStyle.marginTop.computedValue + childRenderStyle.marginBottom.computedValue; + childOffsetX += childRenderStyle.marginLeft.computedValue + + childRenderStyle.marginRight.computedValue; + childOffsetY += childRenderStyle.marginTop.computedValue + + childRenderStyle.marginBottom.computedValue; // Add offset of position relative. // Offset of position absolute and fixed is added in layout stage of positioned renderBox. - final Offset? relativeOffset = CSSPositionedLayout.getRelativeOffset(childRenderStyle); + final Offset? relativeOffset = + CSSPositionedLayout.getRelativeOffset(childRenderStyle); if (relativeOffset != null) { childOffsetX += relativeOffset.dx; childOffsetY += relativeOffset.dy; } // Add offset of transform. - final Offset? transformOffset = child.renderStyle.effectiveTransformOffset; + final Offset? transformOffset = + child.renderStyle.effectiveTransformOffset; if (transformOffset != null) { childOffsetX += transformOffset.dx; childOffsetY += transformOffset.dy; childTransformMainOverflow = math.max( 0, - _isHorizontalFlexDirection ? transformOffset.dx : transformOffset.dy, + _isHorizontalFlexDirection + ? transformOffset.dx + : transformOffset.dy, ); } } final Size childSize = _getChildSize(child)!; - final double childBoxMainSize = _isHorizontalFlexDirection ? childSize.width : childSize.height; - final double childBoxCrossSize = _isHorizontalFlexDirection ? childSize.height : childSize.width; - final double childCrossOffset = _isHorizontalFlexDirection ? childOffsetY : childOffsetX; + final double childBoxMainSize = + _isHorizontalFlexDirection ? childSize.width : childSize.height; + final double childBoxCrossSize = + _isHorizontalFlexDirection ? childSize.height : childSize.width; + final double childCrossOffset = + _isHorizontalFlexDirection ? childOffsetY : childOffsetX; final double childScrollableMainExtent = _isHorizontalFlexDirection ? childScrollableSize.width + childTransformMainOverflow : childScrollableSize.height + childTransformMainOverflow; @@ -5635,25 +6301,28 @@ class RenderFlexLayout extends RenderLayoutBox { // instead of the pre-alignment stacked size. This prevents blank trailing // scroll range after children are shifted by negative leading space. final double childScrollableMain = math.max( - 0, - childMainPosition - physicalMainAxisStartBorder, - ) + + 0, + childMainPosition - physicalMainAxisStartBorder, + ) + math.max(childBoxMainSize, childScrollableMainExtent) + childPhysicalMainEndMargin; final double childScrollableCross = math.max( - childBoxCrossSize + childCrossOffset, - childScrollableCrossExtent); + childBoxCrossSize + childCrossOffset, childScrollableCrossExtent); - maxScrollableMainSizeOfLine = math.max(maxScrollableMainSizeOfLine, childScrollableMain); - maxScrollableCrossSizeInLine = math.max(maxScrollableCrossSizeInLine, childScrollableCross); + maxScrollableMainSizeOfLine = + math.max(maxScrollableMainSizeOfLine, childScrollableMain); + maxScrollableCrossSizeInLine = + math.max(maxScrollableCrossSizeInLine, childScrollableCross); // Update running main size for subsequent siblings (border-box size + main-axis margins). double childMainSize = _getMainSize(child); if (child is RenderBoxModel) { if (_isHorizontalFlexDirection) { - childMainSize += child.renderStyle.marginLeft.computedValue + child.renderStyle.marginRight.computedValue; + childMainSize += child.renderStyle.marginLeft.computedValue + + child.renderStyle.marginRight.computedValue; } else { - childMainSize += child.renderStyle.marginTop.computedValue + child.renderStyle.marginBottom.computedValue; + childMainSize += child.renderStyle.marginTop.computedValue + + child.renderStyle.marginBottom.computedValue; } } preSiblingsMainSize += childMainSize; @@ -5664,7 +6333,8 @@ class RenderFlexLayout extends RenderLayoutBox { } // Max scrollable cross size of all the children in the line. - final double maxScrollableCrossSizeOfLine = preLinesCrossSize + maxScrollableCrossSizeInLine; + final double maxScrollableCrossSizeOfLine = + preLinesCrossSize + maxScrollableCrossSizeInLine; scrollableMainSizeOfLines.add(maxScrollableMainSizeOfLine); scrollableCrossSizeOfLines.add(maxScrollableCrossSizeOfLine); @@ -5679,12 +6349,13 @@ class RenderFlexLayout extends RenderLayoutBox { double maxScrollableMainSizeOfLines = scrollableMainSizeOfLines.isEmpty ? 0 : scrollableMainSizeOfLines.reduce((double curr, double next) { - return curr > next ? curr : next; - }); + return curr > next ? curr : next; + }); RenderBoxModel container = this; - bool isScrollContainer = renderStyle.effectiveOverflowX != CSSOverflowType.visible || - renderStyle.effectiveOverflowY != CSSOverflowType.visible; + bool isScrollContainer = + renderStyle.effectiveOverflowX != CSSOverflowType.visible || + renderStyle.effectiveOverflowY != CSSOverflowType.visible; // Child positions already include physical start padding. Only the trailing // padding needs to be added here. @@ -5695,8 +6366,8 @@ class RenderFlexLayout extends RenderLayoutBox { double maxScrollableCrossSizeOfLines = scrollableCrossSizeOfLines.isEmpty ? 0 : scrollableCrossSizeOfLines.reduce((double curr, double next) { - return curr > next ? curr : next; - }); + return curr > next ? curr : next; + }); // Padding in the end direction of axis should be included in scroll container. double maxScrollableCrossSizeOfChildren = maxScrollableCrossSizeOfLines + @@ -5710,9 +6381,15 @@ class RenderFlexLayout extends RenderLayoutBox { container.renderStyle.effectiveBorderTopWidth.computedValue - container.renderStyle.effectiveBorderBottomWidth.computedValue; double maxScrollableMainSize = math.max( - _isHorizontalFlexDirection ? containerContentWidth : containerContentHeight, maxScrollableMainSizeOfChildren); + _isHorizontalFlexDirection + ? containerContentWidth + : containerContentHeight, + maxScrollableMainSizeOfChildren); double maxScrollableCrossSize = math.max( - _isHorizontalFlexDirection ? containerContentHeight : containerContentWidth, maxScrollableCrossSizeOfChildren); + _isHorizontalFlexDirection + ? containerContentHeight + : containerContentWidth, + maxScrollableCrossSizeOfChildren); scrollableSize = _isHorizontalFlexDirection ? Size(maxScrollableMainSize, maxScrollableCrossSize) @@ -5720,10 +6397,13 @@ class RenderFlexLayout extends RenderLayoutBox { } // Get the cross size of flex line based on flex-wrap and align-items/align-self properties. - double _getFlexLineCrossSize(RenderBox child, - double runCrossAxisExtent, - double runBetweenSpace,) { - bool isSingleLine = (renderStyle.flexWrap != FlexWrap.wrap && renderStyle.flexWrap != FlexWrap.wrapReverse); + double _getFlexLineCrossSize( + RenderBox child, + double runCrossAxisExtent, + double runBetweenSpace, + ) { + bool isSingleLine = (renderStyle.flexWrap != FlexWrap.wrap && + renderStyle.flexWrap != FlexWrap.wrapReverse); if (isSingleLine) { final bool hasDefiniteContainerCross = _hasDefiniteContainerCrossSize(); @@ -5734,11 +6414,14 @@ class RenderFlexLayout extends RenderLayoutBox { // flex containers the flex line’s cross size equals the flex container’s inner cross size // when that size is definite. // https://www.w3.org/TR/css-flexbox-1/#algo-cross-line - double? explicitContainerCross; // from explicit non-auto width/height - double? resolvedContainerCross; // resolved cross size for block-level flex when auto - double? minCrossFromConstraints; // content-box min cross size - double? minCrossFromStyle; // content-box min cross size derived from min-width/min-height - double? containerInnerCross; // measured inner cross size from this layout pass + double? explicitContainerCross; // from explicit non-auto width/height + double? + resolvedContainerCross; // resolved cross size for block-level flex when auto + double? minCrossFromConstraints; // content-box min cross size + double? + minCrossFromStyle; // content-box min cross size derived from min-width/min-height + double? + containerInnerCross; // measured inner cross size from this layout pass final CSSDisplay effectiveDisplay = renderStyle.effectiveDisplay; final bool isInlineFlex = effectiveDisplay == CSSDisplay.inlineFlex; final CSSWritingMode wm = renderStyle.writingMode; @@ -5754,27 +6437,33 @@ class RenderFlexLayout extends RenderLayoutBox { // Row: cross axis is height. final double maxH = constraints.maxHeight; if (maxH.isFinite) { - final double borderV = renderStyle.effectiveBorderTopWidth.computedValue + - renderStyle.effectiveBorderBottomWidth.computedValue; - final double paddingV = renderStyle.paddingTop.computedValue + renderStyle.paddingBottom.computedValue; + final double borderV = + renderStyle.effectiveBorderTopWidth.computedValue + + renderStyle.effectiveBorderBottomWidth.computedValue; + final double paddingV = renderStyle.paddingTop.computedValue + + renderStyle.paddingBottom.computedValue; availableInnerCross = math.max(0, maxH - borderV - paddingV); } } else { // Column: cross axis is width. final double maxW = constraints.maxWidth; if (maxW.isFinite) { - final double borderH = renderStyle.effectiveBorderLeftWidth.computedValue + - renderStyle.effectiveBorderRightWidth.computedValue; - final double paddingH = renderStyle.paddingLeft.computedValue + renderStyle.paddingRight.computedValue; + final double borderH = + renderStyle.effectiveBorderLeftWidth.computedValue + + renderStyle.effectiveBorderRightWidth.computedValue; + final double paddingH = renderStyle.paddingLeft.computedValue + + renderStyle.paddingRight.computedValue; availableInnerCross = math.max(0, maxW - borderH - paddingH); } } } double clampToAvailable(double value) { - if (availableInnerCross == null || !availableInnerCross.isFinite) return value; + if (availableInnerCross == null || !availableInnerCross.isFinite) + return value; return value > availableInnerCross ? availableInnerCross : value; } + if (_isHorizontalFlexDirection) { // Row: cross is height // Only treat as definite if height is explicitly specified (not auto) @@ -5782,7 +6471,9 @@ class RenderFlexLayout extends RenderLayoutBox { explicitContainerCross = renderStyle.contentBoxLogicalHeight; } // Also consider the actually measured inner cross size from this layout pass. - if (hasDefiniteContainerCross && contentSize.height.isFinite && contentSize.height > 0) { + if (hasDefiniteContainerCross && + contentSize.height.isFinite && + contentSize.height > 0) { containerInnerCross = contentSize.height; } // min-height should also participate in establishing the line cross size @@ -5790,14 +6481,17 @@ class RenderFlexLayout extends RenderLayoutBox { // is otherwise indefinite (e.g. height:auto under grid layout constraints). if (renderStyle.minHeight.isNotAuto) { final double minBorderBox = renderStyle.minHeight.computedValue; - double minContentBox = renderStyle.deflatePaddingBorderHeight(minBorderBox); + double minContentBox = + renderStyle.deflatePaddingBorderHeight(minBorderBox); if (minContentBox.isFinite && minContentBox < 0) minContentBox = 0; if (minContentBox.isFinite && minContentBox > 0) { minCrossFromStyle = minContentBox; } } // Height:auto is generally not definite prior to layout; still capture a min-cross constraint if present. - if (contentConstraints != null && contentConstraints!.minHeight.isFinite && contentConstraints!.minHeight > 0) { + if (contentConstraints != null && + contentConstraints!.minHeight.isFinite && + contentConstraints!.minHeight > 0) { minCrossFromConstraints = contentConstraints!.minHeight; } } else { @@ -5808,9 +6502,13 @@ class RenderFlexLayout extends RenderLayoutBox { } // For block-level flex with width:auto in horizontal writing mode, the used width // is fill-available and thus definite; only then may we resolve from constraints. - if (hasDefiniteContainerCross && !isInlineFlex && (explicitContainerCross == null) && crossIsWidth && + if (hasDefiniteContainerCross && + !isInlineFlex && + (explicitContainerCross == null) && + crossIsWidth && wm == CSSWritingMode.horizontalTb) { - if (contentConstraints != null && contentConstraints!.hasBoundedWidth && + if (contentConstraints != null && + contentConstraints!.hasBoundedWidth && contentConstraints!.maxWidth.isFinite) { resolvedContainerCross = contentConstraints!.maxWidth; } @@ -5819,26 +6517,34 @@ class RenderFlexLayout extends RenderLayoutBox { // content width as a definite line cross size; width:auto should shrink-to-fit // in vertical writing modes. Only consider the measured inner width when the // container has an explicit (non-auto) width. - if (hasDefiniteContainerCross && renderStyle.width.isNotAuto && contentSize.width.isFinite && contentSize.width > 0) { + if (hasDefiniteContainerCross && + renderStyle.width.isNotAuto && + contentSize.width.isFinite && + contentSize.width > 0) { containerInnerCross = contentSize.width; } if (renderStyle.minWidth.isNotAuto) { final double minBorderBox = renderStyle.minWidth.computedValue; - double minContentBox = renderStyle.deflatePaddingBorderWidth(minBorderBox); + double minContentBox = + renderStyle.deflatePaddingBorderWidth(minBorderBox); if (minContentBox.isFinite && minContentBox < 0) minContentBox = 0; if (minContentBox.isFinite && minContentBox > 0) { minCrossFromStyle = minContentBox; } } - if (contentConstraints != null && contentConstraints!.minWidth.isFinite && contentConstraints!.minWidth > 0) { + if (contentConstraints != null && + contentConstraints!.minWidth.isFinite && + contentConstraints!.minWidth > 0) { minCrossFromConstraints = contentConstraints!.minWidth; } } // Prefer the larger of the style-derived and constraints-derived minimum cross sizes. if (minCrossFromStyle != null && minCrossFromStyle!.isFinite) { - if (minCrossFromConstraints != null && minCrossFromConstraints!.isFinite) { - minCrossFromConstraints = math.max(minCrossFromConstraints!, minCrossFromStyle!); + if (minCrossFromConstraints != null && + minCrossFromConstraints!.isFinite) { + minCrossFromConstraints = + math.max(minCrossFromConstraints!, minCrossFromStyle!); } else { minCrossFromConstraints = minCrossFromStyle; } @@ -5863,7 +6569,9 @@ class RenderFlexLayout extends RenderLayoutBox { if (containerInnerCross != null && containerInnerCross.isFinite) { return containerInnerCross; } - if (!isInlineFlex && resolvedContainerCross != null && resolvedContainerCross.isFinite) { + if (!isInlineFlex && + resolvedContainerCross != null && + resolvedContainerCross.isFinite) { return resolvedContainerCross; } } @@ -5874,7 +6582,8 @@ class RenderFlexLayout extends RenderLayoutBox { return runCrossAxisExtent; } else { // Flex line of align-content stretch should includes between space. - bool isMultiLineStretch = renderStyle.alignContent == AlignContent.stretch; + bool isMultiLineStretch = + renderStyle.alignContent == AlignContent.stretch; if (isMultiLineStretch) { return runCrossAxisExtent + runBetweenSpace; } else { @@ -5884,7 +6593,9 @@ class RenderFlexLayout extends RenderLayoutBox { } // Set children offset based on alignment properties. - void _setChildrenOffset(List<_RunMetrics> runMetrics,) { + void _setChildrenOffset( + List<_RunMetrics> runMetrics, + ) { if (runMetrics.isEmpty) return; final bool isHorizontal = _isHorizontalFlexDirection; @@ -5923,19 +6634,23 @@ class RenderFlexLayout extends RenderLayoutBox { // intrinsic contentSize to the actual inner box size from Flutter // constraints so that alignment (justify-content/align-items) operates // within the visible box instead of an unconstrained CSS height/width. - final double borderLeft = renderStyle.effectiveBorderLeftWidth.computedValue; - final double borderRight = renderStyle.effectiveBorderRightWidth.computedValue; - final double borderTop = renderStyle.effectiveBorderTopWidth.computedValue; - final double borderBottom = renderStyle.effectiveBorderBottomWidth.computedValue; + final double borderLeft = + renderStyle.effectiveBorderLeftWidth.computedValue; + final double borderRight = + renderStyle.effectiveBorderRightWidth.computedValue; + final double borderTop = + renderStyle.effectiveBorderTopWidth.computedValue; + final double borderBottom = + renderStyle.effectiveBorderBottomWidth.computedValue; final double paddingLeft = renderStyle.paddingLeft.computedValue; final double paddingRight = renderStyle.paddingRight.computedValue; final double paddingTop = renderStyle.paddingTop.computedValue; final double paddingBottom = renderStyle.paddingBottom.computedValue; - final double innerWidthFromSize = - math.max(0.0, size.width - borderLeft - borderRight - paddingLeft - paddingRight); - final double innerHeightFromSize = - math.max(0.0, size.height - borderTop - borderBottom - paddingTop - paddingBottom); + final double innerWidthFromSize = math.max(0.0, + size.width - borderLeft - borderRight - paddingLeft - paddingRight); + final double innerHeightFromSize = math.max(0.0, + size.height - borderTop - borderBottom - paddingTop - paddingBottom); double contentMainExtent; double contentCrossExtent; @@ -5961,7 +6676,8 @@ class RenderFlexLayout extends RenderLayoutBox { if (!mainAxisContentSize.isFinite || mainAxisContentSize <= 0) { mainAxisContentSize = innerMainFromSize; } else { - mainAxisContentSize = math.min(mainAxisContentSize, innerMainFromSize); + mainAxisContentSize = + math.min(mainAxisContentSize, innerMainFromSize); } } crossAxisContentSize = contentCrossExtent; @@ -5969,7 +6685,8 @@ class RenderFlexLayout extends RenderLayoutBox { if (!crossAxisContentSize.isFinite || crossAxisContentSize <= 0) { crossAxisContentSize = innerCrossFromSize; } else { - crossAxisContentSize = math.min(crossAxisContentSize, innerCrossFromSize); + crossAxisContentSize = + math.min(crossAxisContentSize, innerCrossFromSize); } } } else { @@ -6020,7 +6737,9 @@ class RenderFlexLayout extends RenderLayoutBox { if (remainingSpace < 0) { betweenSpace = 0.0; } else { - betweenSpace = runChildrenCount > 1 ? remainingSpace / (runChildrenCount - 1) : 0.0; + betweenSpace = runChildrenCount > 1 + ? remainingSpace / (runChildrenCount - 1) + : 0.0; } break; case JustifyContent.spaceAround: @@ -6028,7 +6747,8 @@ class RenderFlexLayout extends RenderLayoutBox { leadingSpace = remainingSpace / 2.0; betweenSpace = 0.0; } else { - betweenSpace = runChildrenCount > 0 ? remainingSpace / runChildrenCount : 0.0; + betweenSpace = + runChildrenCount > 0 ? remainingSpace / runChildrenCount : 0.0; leadingSpace = betweenSpace / 2.0; } break; @@ -6037,7 +6757,9 @@ class RenderFlexLayout extends RenderLayoutBox { leadingSpace = remainingSpace / 2.0; betweenSpace = 0.0; } else { - betweenSpace = runChildrenCount > 0 ? remainingSpace / (runChildrenCount + 1) : 0.0; + betweenSpace = runChildrenCount > 0 + ? remainingSpace / (runChildrenCount + 1) + : 0.0; leadingSpace = betweenSpace; } break; @@ -6063,17 +6785,23 @@ class RenderFlexLayout extends RenderLayoutBox { // Main axis position of child on layout. double childMainPosition = flipMainAxis - ? mainAxisStartPadding + mainAxisStartBorder + mainAxisContentSize - leadingSpace + ? mainAxisStartPadding + + mainAxisStartBorder + + mainAxisContentSize - + leadingSpace : leadingSpace + mainAxisStartPadding + mainAxisStartBorder; - final bool runAllChildrenAtMaxCross = _areAllRunChildrenAtMaxCrossExtent(runChildrenList, runCrossAxisExtent); + final bool runAllChildrenAtMaxCross = _areAllRunChildrenAtMaxCrossExtent( + runChildrenList, runCrossAxisExtent); // Per-auto-margin main-axis share. Auto margins in the main axis absorb free space. // https://www.w3.org/TR/css-flexbox-1/#auto-margins - final double perMainAxisAutoMargin = - mainAxisAutoMarginCount == 0 ? 0.0 : (math.max(0, remainingSpace) / mainAxisAutoMarginCount); + final double perMainAxisAutoMargin = mainAxisAutoMarginCount == 0 + ? 0.0 + : (math.max(0, remainingSpace) / mainAxisAutoMarginCount); - final bool mainAxisStartAtPhysicalStart = _isMainAxisStartAtPhysicalStart(); + final bool mainAxisStartAtPhysicalStart = + _isMainAxisStartAtPhysicalStart(); for (_RunChild runChild in runChildrenList) { RenderBox child = runChild.child; @@ -6085,23 +6813,36 @@ class RenderFlexLayout extends RenderLayoutBox { // Position the child along the main axis respecting direction. final bool mainAxisStartAuto = isHorizontal - ? (mainAxisStartAtPhysicalStart ? runChild.marginLeftAuto : runChild.marginRightAuto) - : (mainAxisStartAtPhysicalStart ? runChild.marginTopAuto : runChild.marginBottomAuto); + ? (mainAxisStartAtPhysicalStart + ? runChild.marginLeftAuto + : runChild.marginRightAuto) + : (mainAxisStartAtPhysicalStart + ? runChild.marginTopAuto + : runChild.marginBottomAuto); final bool mainAxisEndAuto = isHorizontal - ? (mainAxisStartAtPhysicalStart ? runChild.marginRightAuto : runChild.marginLeftAuto) - : (mainAxisStartAtPhysicalStart ? runChild.marginBottomAuto : runChild.marginTopAuto); - final double startAutoSpace = mainAxisStartAuto ? perMainAxisAutoMargin : 0.0; - final double endAutoSpace = mainAxisEndAuto ? perMainAxisAutoMargin : 0.0; + ? (mainAxisStartAtPhysicalStart + ? runChild.marginRightAuto + : runChild.marginLeftAuto) + : (mainAxisStartAtPhysicalStart + ? runChild.marginBottomAuto + : runChild.marginTopAuto); + final double startAutoSpace = + mainAxisStartAuto ? perMainAxisAutoMargin : 0.0; + final double endAutoSpace = + mainAxisEndAuto ? perMainAxisAutoMargin : 0.0; if (flipMainAxis) { // In reversed main axis (e.g., column-reverse or RTL row), advance from the // far edge by the start margin and the child's own size. Do not subtract the // trailing (end) margin here — it separates this item from the next. - final double adjStartMargin = _calculateMainAxisMarginForJustContentType(childStartMargin); - childMainPosition -= (startAutoSpace + adjStartMargin + childMainSizeOnly); + final double adjStartMargin = + _calculateMainAxisMarginForJustContentType(childStartMargin); + childMainPosition -= + (startAutoSpace + adjStartMargin + childMainSizeOnly); } else { // In normal flow, advance by the start margin before placing. - childMainPosition += _calculateMainAxisMarginForJustContentType(childStartMargin); + childMainPosition += + _calculateMainAxisMarginForJustContentType(childStartMargin); childMainPosition += startAutoSpace; } double? childCrossPosition; @@ -6113,11 +6854,13 @@ class RenderFlexLayout extends RenderLayoutBox { case AlignSelf.flexStart: case AlignSelf.start: case AlignSelf.stretch: - alignment = renderStyle.flexWrap == FlexWrap.wrapReverse ? 'end' : 'start'; + alignment = + renderStyle.flexWrap == FlexWrap.wrapReverse ? 'end' : 'start'; break; case AlignSelf.flexEnd: case AlignSelf.end: - alignment = renderStyle.flexWrap == FlexWrap.wrapReverse ? 'start' : 'end'; + alignment = + renderStyle.flexWrap == FlexWrap.wrapReverse ? 'start' : 'end'; break; case AlignSelf.center: alignment = 'center'; @@ -6131,18 +6874,22 @@ class RenderFlexLayout extends RenderLayoutBox { case AlignItems.flexStart: case AlignItems.start: case AlignItems.stretch: - alignment = renderStyle.flexWrap == FlexWrap.wrapReverse ? 'end' : 'start'; + alignment = renderStyle.flexWrap == FlexWrap.wrapReverse + ? 'end' + : 'start'; break; case AlignItems.flexEnd: case AlignItems.end: - alignment = renderStyle.flexWrap == FlexWrap.wrapReverse ? 'start' : 'end'; + alignment = renderStyle.flexWrap == FlexWrap.wrapReverse + ? 'start' + : 'end'; break; case AlignItems.center: alignment = 'center'; break; case AlignItems.baseline: case AlignItems.lastBaseline: - // FIXME: baseline alignment in wrap-reverse flexWrap may display different from browser in some case + // FIXME: baseline alignment in wrap-reverse flexWrap may display different from browser in some case if (isHorizontal) { alignment = 'baseline'; } else if (renderStyle.flexWrap == FlexWrap.wrapReverse) { @@ -6161,7 +6908,9 @@ class RenderFlexLayout extends RenderLayoutBox { // Text is aligned in anonymous block container rather than flexbox container. // https://www.w3.org/TR/css-flexbox-1/#flex-items - if (renderStyle.alignItems == AlignItems.stretch && child is RenderTextBox && !isHorizontal) { + if (renderStyle.alignItems == AlignItems.stretch && + child is RenderTextBox && + !isHorizontal) { TextAlign textAlign = renderStyle.textAlign; if (textAlign == TextAlign.start) { alignment = 'start'; @@ -6193,24 +6942,29 @@ class RenderFlexLayout extends RenderLayoutBox { // https://www.w3.org/TR/css-flexbox-1/#auto-margins if (child is RenderBoxModel) { // Margin auto does not work with negative remaining space. - final double crossAxisRemainingSpace = math.max(0, crossAxisContentSize - childCrossAxisExtent); + final double crossAxisRemainingSpace = + math.max(0, crossAxisContentSize - childCrossAxisExtent); if (isHorizontal) { // Cross axis is vertical (top/bottom). if (runChild.marginTopAuto) { if (runChild.marginBottomAuto) { - childCrossPosition = childCrossPosition! + crossAxisRemainingSpace / 2; + childCrossPosition = + childCrossPosition! + crossAxisRemainingSpace / 2; } else { - childCrossPosition = childCrossPosition! + crossAxisRemainingSpace; + childCrossPosition = + childCrossPosition! + crossAxisRemainingSpace; } } } else { // Cross axis is horizontal (left/right). if (runChild.marginLeftAuto) { if (runChild.marginRightAuto) { - childCrossPosition = childCrossPosition! + crossAxisRemainingSpace / 2; + childCrossPosition = + childCrossPosition! + crossAxisRemainingSpace / 2; } else { - childCrossPosition = childCrossPosition! + crossAxisRemainingSpace; + childCrossPosition = + childCrossPosition! + crossAxisRemainingSpace; } } } @@ -6220,8 +6974,11 @@ class RenderFlexLayout extends RenderLayoutBox { double crossOffset; if (renderStyle.flexWrap == FlexWrap.wrapReverse) { - crossOffset = - childCrossPosition! + (crossAxisContentSize - crossAxisOffset - runCrossAxisExtent - runBetweenSpace); + crossOffset = childCrossPosition! + + (crossAxisContentSize - + crossAxisOffset - + runCrossAxisExtent - + runBetweenSpace); } else { crossOffset = childCrossPosition! + crossAxisOffset; } @@ -6238,10 +6995,15 @@ class RenderFlexLayout extends RenderLayoutBox { if (flipMainAxis) { // After placing in reversed flow, move past the trailing (end) margin, // then account for between-space and gaps. - childMainPosition -= (childEndMargin + endAutoSpace + betweenSpace + effectiveGap); + childMainPosition -= + (childEndMargin + endAutoSpace + betweenSpace + effectiveGap); } else { // Normal flow: advance by the child size, trailing margin, between-space and gaps. - childMainPosition += (childMainSizeOnly + childEndMargin + endAutoSpace + betweenSpace + effectiveGap); + childMainPosition += (childMainSizeOnly + + childEndMargin + + endAutoSpace + + betweenSpace + + effectiveGap); } } @@ -6304,14 +7066,16 @@ class RenderFlexLayout extends RenderLayoutBox { // Replaced elements (e.g., ) with an intrinsic aspect ratio should not be // stretched in the cross axis; browsers keep their border-box proportional even // under align-items: stretch. This matches CSS Flexbox §9.4. - if (_shouldPreserveIntrinsicRatio(childBoxModel, hasDefiniteContainerCross: hasDefiniteCrossSize)) { + if (_shouldPreserveIntrinsicRatio(childBoxModel, + hasDefiniteContainerCross: hasDefiniteCrossSize)) { return false; } return true; } - bool _shouldPreserveIntrinsicRatio(RenderBoxModel child, {required bool hasDefiniteContainerCross}) { + bool _shouldPreserveIntrinsicRatio(RenderBoxModel child, + {required bool hasDefiniteContainerCross}) { if (child is! RenderReplaced) { return false; } @@ -6330,57 +7094,78 @@ class RenderFlexLayout extends RenderLayoutBox { bool _hasDefiniteContainerCrossSize() { if (_isHorizontalFlexDirection) { if (renderStyle.contentBoxLogicalHeight != null) return true; - if (contentConstraints != null && contentConstraints!.hasTightHeight) return true; + if (contentConstraints != null && contentConstraints!.hasTightHeight) + return true; if (constraints.hasTightHeight) return true; return false; } else { if (renderStyle.contentBoxLogicalWidth != null) return true; - if (contentConstraints != null && contentConstraints!.hasTightWidth) return true; + if (contentConstraints != null && contentConstraints!.hasTightWidth) + return true; if (constraints.hasTightWidth) return true; return false; } } // Get child stretched size in the cross axis. - double _getChildStretchedCrossSize(RenderBoxModel child, - double runCrossAxisExtent, - double runBetweenSpace,) { - bool isFlexWrap = renderStyle.flexWrap == FlexWrap.wrap || renderStyle.flexWrap == FlexWrap.wrapReverse; - double childCrossAxisMargin = _horizontalMarginNegativeSet(0, child, isHorizontal: !_isHorizontalFlexDirection); - _isHorizontalFlexDirection ? child.renderStyle.margin.vertical : child.renderStyle.margin.horizontal; - double maxCrossSizeConstraints = _isHorizontalFlexDirection ? constraints.maxHeight : constraints.maxWidth; - double flexLineCrossSize = _getFlexLineCrossSize(child, runCrossAxisExtent, runBetweenSpace); + double _getChildStretchedCrossSize( + RenderBoxModel child, + double runCrossAxisExtent, + double runBetweenSpace, + ) { + bool isFlexWrap = renderStyle.flexWrap == FlexWrap.wrap || + renderStyle.flexWrap == FlexWrap.wrapReverse; + double childCrossAxisMargin = _horizontalMarginNegativeSet(0, child, + isHorizontal: !_isHorizontalFlexDirection); + _isHorizontalFlexDirection + ? child.renderStyle.margin.vertical + : child.renderStyle.margin.horizontal; + double maxCrossSizeConstraints = _isHorizontalFlexDirection + ? constraints.maxHeight + : constraints.maxWidth; + double flexLineCrossSize = + _getFlexLineCrossSize(child, runCrossAxisExtent, runBetweenSpace); // Should subtract margin when stretch flex item. double childStretchedCrossSize = flexLineCrossSize - childCrossAxisMargin; // Flex line cross size should not exceed container's cross size if specified when flex-wrap is nowrap. if (!isFlexWrap && maxCrossSizeConstraints.isFinite) { - double crossAxisBorder = _isHorizontalFlexDirection ? renderStyle.border.vertical : renderStyle.border.horizontal; - double crossAxisPadding = - _isHorizontalFlexDirection ? renderStyle.padding.vertical : renderStyle.padding.horizontal; - childStretchedCrossSize = - math.min(maxCrossSizeConstraints - crossAxisBorder - crossAxisPadding, childStretchedCrossSize); + double crossAxisBorder = _isHorizontalFlexDirection + ? renderStyle.border.vertical + : renderStyle.border.horizontal; + double crossAxisPadding = _isHorizontalFlexDirection + ? renderStyle.padding.vertical + : renderStyle.padding.horizontal; + childStretchedCrossSize = math.min( + maxCrossSizeConstraints - crossAxisBorder - crossAxisPadding, + childStretchedCrossSize); } // Constrain stretched size by max-width/max-height. double? maxCrossSize; if (_isHorizontalFlexDirection && child.renderStyle.maxHeight.isNotNone) { maxCrossSize = child.renderStyle.maxHeight.computedValue; - } else if (!_isHorizontalFlexDirection && child.renderStyle.maxWidth.isNotNone) { + } else if (!_isHorizontalFlexDirection && + child.renderStyle.maxWidth.isNotNone) { maxCrossSize = child.renderStyle.maxWidth.computedValue; } if (maxCrossSize != null) { - childStretchedCrossSize = childStretchedCrossSize > maxCrossSize ? maxCrossSize : childStretchedCrossSize; + childStretchedCrossSize = childStretchedCrossSize > maxCrossSize + ? maxCrossSize + : childStretchedCrossSize; } // Constrain stretched size by min-width/min-height. double? minCrossSize; if (_isHorizontalFlexDirection && child.renderStyle.minHeight.isNotAuto) { minCrossSize = child.renderStyle.minHeight.computedValue; - } else if (!_isHorizontalFlexDirection && child.renderStyle.minWidth.isNotAuto) { + } else if (!_isHorizontalFlexDirection && + child.renderStyle.minWidth.isNotAuto) { minCrossSize = child.renderStyle.minWidth.computedValue; } if (minCrossSize != null) { - childStretchedCrossSize = childStretchedCrossSize < minCrossSize ? minCrossSize : childStretchedCrossSize; + childStretchedCrossSize = childStretchedCrossSize < minCrossSize + ? minCrossSize + : childStretchedCrossSize; } // Ensure stretched height in row-direction is not smaller than the @@ -6418,27 +7203,30 @@ class RenderFlexLayout extends RenderLayoutBox { final CSSLengthValue marginRight = s.marginRight; final CSSLengthValue marginTop = s.marginTop; final CSSLengthValue marginBottom = s.marginBottom; - if (_isHorizontalFlexDirection && (marginTop.isAuto || marginBottom.isAuto)) return true; - if (!_isHorizontalFlexDirection && (marginLeft.isAuto || marginRight.isAuto)) return true; + if (_isHorizontalFlexDirection && + (marginTop.isAuto || marginBottom.isAuto)) return true; + if (!_isHorizontalFlexDirection && + (marginLeft.isAuto || marginRight.isAuto)) return true; } return false; } // Get flex item cross axis offset by align-items/align-self. - double? _getChildCrossAxisOffset(String alignment, - RenderBox child, - double? childCrossPosition, - double runBaselineExtent, - double runCrossAxisExtent, - double runBetweenSpace, - double crossAxisStartPadding, - double crossAxisStartBorder, { - required double childCrossAxisExtent, - required double childCrossAxisStartMargin, - required double childCrossAxisEndMargin, - required bool hasAutoCrossAxisMargin, - required bool runAllChildrenAtMaxCross, - }) { + double? _getChildCrossAxisOffset( + String alignment, + RenderBox child, + double? childCrossPosition, + double runBaselineExtent, + double runCrossAxisExtent, + double runBetweenSpace, + double crossAxisStartPadding, + double crossAxisStartBorder, { + required double childCrossAxisExtent, + required double childCrossAxisStartMargin, + required double childCrossAxisEndMargin, + required bool hasAutoCrossAxisMargin, + required bool runAllChildrenAtMaxCross, + }) { // Leading between height of line box's content area and line height of line box. double lineBoxLeading = 0; double? lineBoxHeight = _getLineHeight(this); @@ -6452,13 +7240,16 @@ class RenderFlexLayout extends RenderLayoutBox { runBetweenSpace, ); // start offset including margin (used by start/end alignment) - double crossStartAddedOffset = crossAxisStartPadding + crossAxisStartBorder + childCrossAxisStartMargin; + double crossStartAddedOffset = crossAxisStartPadding + + crossAxisStartBorder + + childCrossAxisStartMargin; // start offset without margin (used by center alignment where we center the margin-box itself) double crossStartNoMargin = crossAxisStartPadding + crossAxisStartBorder; final _FlexContainerInvariants? inv = _layoutInvariants; final bool crossIsHorizontal; - final bool crossStartIsPhysicalStart; // left for horizontal, top for vertical + final bool + crossStartIsPhysicalStart; // left for horizontal, top for vertical if (inv != null) { crossIsHorizontal = inv.isCrossAxisHorizontal; crossStartIsPhysicalStart = inv.isCrossAxisStartAtPhysicalStart; @@ -6466,7 +7257,8 @@ class RenderFlexLayout extends RenderLayoutBox { // Determine cross axis orientation and where cross-start maps physically. final CSSWritingMode wm = renderStyle.writingMode; final bool inlineIsHorizontal = (wm == CSSWritingMode.horizontalTb); - if (renderStyle.flexDirection == FlexDirection.row || renderStyle.flexDirection == FlexDirection.rowReverse) { + if (renderStyle.flexDirection == FlexDirection.row || + renderStyle.flexDirection == FlexDirection.rowReverse) { // Cross is block axis. crossIsHorizontal = !inlineIsHorizontal; if (crossIsHorizontal) { @@ -6481,7 +7273,8 @@ class RenderFlexLayout extends RenderLayoutBox { crossIsHorizontal = inlineIsHorizontal; if (crossIsHorizontal) { // Inline-start follows text direction in horizontal-tb. - crossStartIsPhysicalStart = (renderStyle.direction != TextDirection.rtl); + crossStartIsPhysicalStart = + (renderStyle.direction != TextDirection.rtl); } else { // Inline-start is physical top in vertical writing modes. crossStartIsPhysicalStart = true; @@ -6516,16 +7309,20 @@ class RenderFlexLayout extends RenderLayoutBox { childCrossAxisStartMargin; } else { // Cross-start at right: cross-end is left (physical start) - return crossAxisStartPadding + crossAxisStartBorder + childCrossAxisEndMargin; + return crossAxisStartPadding + + crossAxisStartBorder + + childCrossAxisEndMargin; } case 'center': // Center the child's MARGIN-BOX within the flex line's content box (spec behavior). // We first get the child's cross-extent including margins, then derive the // border-box extent for overflow heuristics and logging. - final double childExtentWithMargin = childCrossAxisExtent; // includes margins + final double childExtentWithMargin = + childCrossAxisExtent; // includes margins final double startMargin = childCrossAxisStartMargin; final double endMargin = childCrossAxisEndMargin; - final double borderBoxExtent = math.max(0.0, childExtentWithMargin - (startMargin + endMargin)); + final double borderBoxExtent = + math.max(0.0, childExtentWithMargin - (startMargin + endMargin)); // Center within the content box by default (spec-aligned). // Additionally, for vertical cross-axes (row direction), when the item overflows // the content box (free space < 0) and the container has cross-axis padding, @@ -6543,26 +7340,38 @@ class RenderFlexLayout extends RenderLayoutBox { // centering of padded controls. This preserves expected behavior for // headers/toolbars where padding defines visual bounds. final double padStart = crossAxisStartPadding; - final double padEnd = inv?.crossAxisPaddingEnd ?? _flowAwareCrossAxisPadding(isEnd: true); + final double padEnd = inv?.crossAxisPaddingEnd ?? + _flowAwareCrossAxisPadding(isEnd: true); final double borderStart = crossAxisStartBorder; - final double borderEnd = inv?.crossAxisBorderEnd ?? renderStyle.effectiveBorderBottomWidth.computedValue; - final double padBorderSum = padStart + padEnd + borderStart + borderEnd; + final double borderEnd = inv?.crossAxisBorderEnd ?? + renderStyle.effectiveBorderBottomWidth.computedValue; + final double padBorderSum = + padStart + padEnd + borderStart + borderEnd; final double freeSpace = flexLineCrossSize - borderBoxExtent; const double kEpsilon = 0.0001; final bool isExactFit = freeSpace.abs() <= kEpsilon; - if (padBorderSum > 0 && (freeSpace < 0 || (isExactFit && runAllChildrenAtMaxCross))) { + if (padBorderSum > 0 && + (freeSpace < 0 || (isExactFit && runAllChildrenAtMaxCross))) { // Determine container content cross size (definite if set on style), // fall back to the current line cross size. - final double containerContentCross = renderStyle.contentBoxLogicalHeight ?? flexLineCrossSize; - final double containerBorderCross = containerContentCross + padStart + padEnd + borderStart + borderEnd; - final double posFromBorder = (containerBorderCross - borderBoxExtent) / 2.0; - final double pos = posFromBorder; // since offsets are measured from border-start + final double containerContentCross = + renderStyle.contentBoxLogicalHeight ?? flexLineCrossSize; + final double containerBorderCross = containerContentCross + + padStart + + padEnd + + borderStart + + borderEnd; + final double posFromBorder = + (containerBorderCross - borderBoxExtent) / 2.0; + final double pos = + posFromBorder; // since offsets are measured from border-start return pos.isFinite ? pos : crossStartNoMargin; } } // If the margin-box is equal to or wider than the line cross size, pin to start // to avoid introducing cross-axis offset that would create horizontal scroll. - final double marginBoxExtent = borderBoxExtent + startMargin + endMargin; + final double marginBoxExtent = + borderBoxExtent + startMargin + endMargin; // Only clamp for horizontal cross-axis (i.e., when cross is width), and only when // overflow is caused by margins (border-box fits, margin-box overflows). If the // border-box itself is wider than the line, we still center (allow negative offset) @@ -6570,28 +7379,33 @@ class RenderFlexLayout extends RenderLayoutBox { // Only treat as overflow when the margin-box actually exceeds the line cross size. // If it exactly equals, there is no overflow and centering should place the // border-box at startMargin (i.e., symmetric gaps), matching browser behavior. - final bool marginOnlyOverflow = borderBoxExtent <= flexLineCrossSize && marginBoxExtent > flexLineCrossSize; + final bool marginOnlyOverflow = borderBoxExtent <= flexLineCrossSize && + marginBoxExtent > flexLineCrossSize; if (crossIsHorizontal && marginOnlyOverflow) { return crossStartNoMargin; } // Center the margin-box in the line's content box, then add the start margin // to obtain the border-box offset. final double freeInContent = flexLineCrossSize - marginBoxExtent; - final double pos = crossStartNoMargin + freeInContent / 2.0 + startMargin; + final double pos = + crossStartNoMargin + freeInContent / 2.0 + startMargin; return pos; case 'baseline': - // In column flex-direction (vertical main axis), baseline alignment behaves - // like flex-start per our layout model. Avoid using runBaselineExtent which - // is not computed for vertical-main containers. + // In column flex-direction (vertical main axis), baseline alignment behaves + // like flex-start per our layout model. Avoid using runBaselineExtent which + // is not computed for vertical-main containers. if (!_isHorizontalFlexDirection) { return crossStartAddedOffset; } // Distance from top to baseline of child. double childAscent = _getChildAscent(child); - final double offset = crossStartAddedOffset + lineBoxLeading / 2 + (runBaselineExtent - childAscent); + final double offset = crossStartAddedOffset + + lineBoxLeading / 2 + + (runBaselineExtent - childAscent); if (DebugFlags.debugLogFlexBaselineEnabled) { // ignore: avoid_print - print('[FlexBaseline] offset child=${child.runtimeType}#${child.hashCode} ' + print( + '[FlexBaseline] offset child=${child.runtimeType}#${child.hashCode} ' 'runBaseline=${runBaselineExtent.toStringAsFixed(2)} ' 'childAscent=${childAscent.toStringAsFixed(2)} ' 'lineLeading=${lineBoxLeading.toStringAsFixed(2)} ' @@ -6604,7 +7418,8 @@ class RenderFlexLayout extends RenderLayoutBox { } } - bool _areAllRunChildrenAtMaxCrossExtent(List<_RunChild> runChildren, double runCrossAxisExtent) { + bool _areAllRunChildrenAtMaxCrossExtent( + List<_RunChild> runChildren, double runCrossAxisExtent) { const double kEpsilon = 0.0001; for (final _RunChild runChild in runChildren) { final double childCrossAxisExtent = _getCrossAxisExtent(runChild.child); @@ -6622,7 +7437,8 @@ class RenderFlexLayout extends RenderLayoutBox { } // Get child size through boxSize to avoid flutter error when parentUsesSize is set to false. - Size? _getChildSize(RenderBox? child, {bool shouldUseIntrinsicMainSize = false}) { + Size? _getChildSize(RenderBox? child, + {bool shouldUseIntrinsicMainSize = false}) { Size? childSize; if (child != null) { childSize = _transientChildSizeOverrides?[child]; @@ -6743,10 +7559,14 @@ class RenderFlexLayout extends RenderLayoutBox { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('flexDirection', renderStyle.flexDirection)); - properties.add(DiagnosticsProperty('justifyContent', renderStyle.justifyContent)); - properties.add(DiagnosticsProperty('alignItems', renderStyle.alignItems)); - properties.add(DiagnosticsProperty('flexWrap', renderStyle.flexWrap)); + properties.add(DiagnosticsProperty( + 'flexDirection', renderStyle.flexDirection)); + properties.add(DiagnosticsProperty( + 'justifyContent', renderStyle.justifyContent)); + properties.add( + DiagnosticsProperty('alignItems', renderStyle.alignItems)); + properties + .add(DiagnosticsProperty('flexWrap', renderStyle.flexWrap)); } static bool _isPlaceholderPositioned(RenderObject child) { From 013f63310133a1ee13b5d5c29d4018535d71771c Mon Sep 17 00:00:00 2001 From: andycall Date: Fri, 27 Mar 2026 00:14:58 -0700 Subject: [PATCH 04/13] fix(webf): restore stable flex layout behavior --- .../scripts/profile_hotspots_integration.js | 124 +- .../text_comprehensive_test.ts.8af684eb1.png | Bin 34421 -> 34161 bytes webf/lib/src/foundation/debug_flags.dart | 40 +- webf/lib/src/rendering/flex.dart | 2044 +++++------------ 4 files changed, 726 insertions(+), 1482 deletions(-) diff --git a/integration_tests/scripts/profile_hotspots_integration.js b/integration_tests/scripts/profile_hotspots_integration.js index add9efc846..565bf4cf69 100644 --- a/integration_tests/scripts/profile_hotspots_integration.js +++ b/integration_tests/scripts/profile_hotspots_integration.js @@ -3,6 +3,7 @@ const fsp = require('fs/promises'); const {spawn} = require('child_process'); const os = require('os'); const path = require('path'); +const {fileURLToPath} = require('url'); const PROFILE_CASE_FILTER_DEFINE = '--dart-define=WEBF_PROFILE_CASE_FILTER='; const WORKING_DIRECTORY = path.join(__dirname, '..'); @@ -16,6 +17,105 @@ const PROFILE_OUTPUT_DIRECTORY = path.join( 'build', 'profile_hotspots', ); +const ANDROID_LOCAL_PROPERTIES_PATH = path.join( + WORKING_DIRECTORY, + 'android', + 'local.properties', +); + +function parseLocalProperties() { + if (!fs.existsSync(ANDROID_LOCAL_PROPERTIES_PATH)) { + return {}; + } + + const source = fs.readFileSync(ANDROID_LOCAL_PROPERTIES_PATH, 'utf8'); + const properties = {}; + for (const line of source.split(/\r?\n/)) { + if (!line || line.trimStart().startsWith('#')) continue; + const separatorIndex = line.indexOf('='); + if (separatorIndex === -1) continue; + const key = line.slice(0, separatorIndex).trim(); + const value = line.slice(separatorIndex + 1).trim(); + properties[key] = value; + } + return properties; +} + +function resolveFlutterRootFromPackageConfig() { + const packageConfigPath = path.join(WORKING_DIRECTORY, '.dart_tool', 'package_config.json'); + if (!fs.existsSync(packageConfigPath)) { + return null; + } + + try { + const packageConfig = JSON.parse(fs.readFileSync(packageConfigPath, 'utf8')); + if (!packageConfig.flutterRoot) { + return null; + } + return path.resolve(fileURLToPath(packageConfig.flutterRoot)); + } catch (_) { + return null; + } +} + +function resolveFlutterBinary() { + const localProperties = parseLocalProperties(); + const flutterRoot = + localProperties['flutter.sdk'] || + process.env.FLUTTER_ROOT || + process.env.FLUTTER_SDK; + if (!flutterRoot) { + return 'flutter'; + } + return path.join(flutterRoot, 'bin', 'flutter'); +} + +function normalizePath(filePath) { + return path.normalize(path.resolve(filePath)); +} + +function runFlutterCommand(args) { + const flutterBinary = resolveFlutterBinary(); + return new Promise((resolve, reject) => { + const child = spawn(flutterBinary, args, { + cwd: WORKING_DIRECTORY, + env: { + ...process.env, + NO_PROXY: process.env.NO_PROXY || '127.0.0.1,localhost', + no_proxy: process.env.no_proxy || '127.0.0.1,localhost', + }, + stdio: 'inherit', + }); + + child.on('close', (code) => { + if ((code ?? 1) === 0) { + resolve(); + return; + } + reject(new Error(`${path.basename(flutterBinary)} ${args.join(' ')} exited with code ${code ?? 1}`)); + }); + + child.on('error', reject); + }); +} + +async function ensureFlutterPackageConfigIsCurrent() { + const configuredFlutterBinary = resolveFlutterBinary(); + const configuredFlutterRoot = normalizePath(path.dirname(path.dirname(configuredFlutterBinary))); + const packageConfigFlutterRoot = resolveFlutterRootFromPackageConfig(); + + if (packageConfigFlutterRoot && normalizePath(packageConfigFlutterRoot) === configuredFlutterRoot) { + return; + } + + console.log( + `Refreshing Flutter package config for ${configuredFlutterRoot}` + + (packageConfigFlutterRoot + ? ` (was ${normalizePath(packageConfigFlutterRoot)})` + : ' (package_config.json missing Flutter root)'), + ); + await runFlutterCommand(['pub', 'get']); +} function getDefaultDeviceName() { if (os.platform() === 'darwin') return 'macos'; @@ -92,27 +192,7 @@ function buildFlutterDriveArgs(deviceName, extraArgs) { } function runFlutterDrive(args) { - return new Promise((resolve, reject) => { - const child = spawn('flutter', args, { - cwd: WORKING_DIRECTORY, - env: { - ...process.env, - NO_PROXY: process.env.NO_PROXY || '127.0.0.1,localhost', - no_proxy: process.env.no_proxy || '127.0.0.1,localhost', - }, - stdio: 'inherit', - }); - - child.on('close', (code) => { - if ((code ?? 1) === 0) { - resolve(); - return; - } - reject(new Error(`flutter drive exited with code ${code ?? 1}`)); - }); - - child.on('error', reject); - }); + return runFlutterCommand(args); } async function readJson(filePath) { @@ -165,6 +245,8 @@ async function main() { const {requestedCaseIds, remainingArgs} = extractRequestedCaseIds(deviceArgs); const requestedOrAllCaseIds = requestedCaseIds ?? loadProfileCaseIds(); + await ensureFlutterPackageConfigIsCurrent(); + if (isMobileDevice(deviceName)) { await runMobileCasesIndividually({ deviceName, diff --git a/integration_tests/snapshots/css/css-text-mixin/text_comprehensive_test.ts.8af684eb1.png b/integration_tests/snapshots/css/css-text-mixin/text_comprehensive_test.ts.8af684eb1.png index 9e1abe1c191603292f7338b6fee6539829406314..c4d60cc1647770307cf15562ec5cc09d2702b16e 100644 GIT binary patch literal 34161 zcmd?RWmsEXx2TO3O0iOkTk%rd9ZG>xyhx$AySrP_7I(MeUR(kJ8j8ESI|PCS36PWL zecpYpZRh&VK70TB_%TD)Vyz@=&5SwcJ?_Ex&&skmFUVdXAtB+&eUer~LPEAeLP808 zj*i$;N48apctCblll_2HGe&WUc!K8oK~Cd2;>Yi~StJtD8zebtNe!>;lT}YY4R;{? zd_LpK;E)elTAKL>mMVT2A)$7ifp$;cz!&11qoY6JH4b%E&#(;Un&&F#+iNI~UpzOF zR+U6Udqv9}A9^L6#JqpgZz<215o-2oCqHjryq?E(&lk8aV!7iF-1i0Yn5LIl|4ddJ zalsf0z!^lxL2b`d2OY2($VXZGoD+c04pcMQ!LO?c}bAxQ)`SeZTS>4Fz_|t_Q`N& zmWY5dXF#qCXy4_&0G_pbII_kz`dy@OIDHezZV#wwewj8rxp~P|$4pOu1)e)rl`nt9 zWXW&wyAzGU*AD;t3$8?DZV2>GJCJ4#im@xX3)#=!EOix7wh1rhuU; zh+k)A;e_4sbJNow*21JFpFXFog_F~Ka7>;Hsj{q?DwmvoW$)E2vRC_)g5k%g(xiD? zlh$@BsQ#pyZg63Jbn3yCLA4gMyA`G*_R=W|AnHm|mh*hZ_5;z~UMI|X>G*)>#Og$8 zR^x0jys$2_Xk;^b?2c{Fzy85Is-?zkd~DphjFtW!|M#lzT?IQ4d`9i0{7-K8ap@@R zc;Tz0ujB5>4N7gs`4eGg&b>3wkY=<_7~xqhWwi@2(*`Qi8m#`|)NJEfQ&<>QWp7K& zhTqu?T_AlT7Yv^i>SACNZ(6m@;Jx&LKf2el_DpIhMf8pQebE`UK~9=H*>W?pN&f3*KdhdCB5jD+2S0ov^2EM~&qC}S^e zYfLB6r9h&Ji&8ohjhr?(s~tCW?A2Chj^R$AYD9i^Y=Gq!iBq-$3`U^~2bFTz-RNM` zIawIlI-9z-a%Jb{?>;*=EIk0kJzg&=TD={#bUo*e75BwtM*TSHI54>rc+QbI{9{oB z!gC)d9!pu7SK8Jb9a;7jJytk3(NUsr&k1&H&CXV0?&L1$n51^}lGp%mX|wMaplt1q z@|X%R6P~VJ+>$Ob6fqb4xH*^ zLsWVZZ|oB|FEUo8;C4A~0IuPaa@+S~mGNT(ODiSY6mAE`3t4@Jnj;(RjR7I8B%v)U z#GTS1vn}edmiK-mZu-VlxsxKU|Idvh91n5gBM-;*GX(((_Gk;@kCh=w4Q;tw*Jq|k zsQu$cJ95NZ&LWFfOJXx=M4#^;EsL;8{={RJssXRD$c8+Vx-Oobr1;jQr5Yj0X4l7; zEl9_T!x)=8Z{9XBh{zq~{Yjq=l+~~^`TExvf9C5MXJ^*o_TvMM zuQO!RY0`1Wj9Zop^}U@MdV;jEpq})i$#)g<_ZoV{{R=5-5-i5h{9@vai9izVu3Yjm z)|xLW9-{0Piiqt#InJ3oUP#3LP==)O(9(WyX4tT+z&Vv%&XJryh$iy< z?8q-n6IQi7zTK~4&Nx|yU0iE%P;6^t9*$ENoJ&s-mO!=H|#H)0yS_2tVAOE~-U ziv`FDa-{E#Wh2!Nyrtb7413Q>H7$nW4$}!>kO$wK>3U0AWct^sehR5|XZ6Qx zuWYtpE-L_*X!X+W)tu>>Jl+x@pmLrtX{q0^cF~UMvW((hIa~hzps-G+&xpi@Xw$j* z*LE95&nfLxvZng$Yk;3T`&&YvbfOA>Mt|F%_7diom(lbthkMPBUc{3fohjJsd0CQteM z_lYE+SYY`id9CPX&dLK_yui|P$W-gifSKr=?%FaBh2PR0wumWJot!^sW=?PMmH9yx z79wccacgFK>~Iyu3cmC5FJ&rAbBOvu^X;vb$5u@_$jT#8@J$w1ltTi#*GTvMsU|YG zZ(U@a8Uz4NG7dKc`aXLM-~$;34zHz8AduYnMH z63)FnfE=l3bXVE(8g_}Cv|c~;8B~zMOHY~BWSf(gR+!cEO}c}?SUZ_wOIBHtnH^W1 z@%*lX!*4gh_sA&tpkajC(VL+`_$^ylNr{2&gk*ySj_A!wkAuwF(0Byw3VKCboKm{fgDv%(NPP%Vh3O=i>eNJ7Do?18<)Ui(R?q5c?HIrE>+0zI`d>+ zcfrCYM+bO^KxVa%zqZxFt+77+7LUPAYy10|mKyCSQ{`U0{<(sDu*RV4w zC8Yyy1_o~NRUHE=Uj89}0OboThV(HBYAVE}95*F!AuLRv<8m(x5*OIL(gxW{dPU*b zg`*;hzi^r>_9_rxUwb@Han}+Z-^YsH&=fw*>@8S;h7TpiJ%(JGeXOglqgAz+3XUWsY&f6Qw>Kh|GG{Wt^?rhp9 zNsNP!T&6`W&_zq9%`knI%TvHB6JR%`2+`2c#(DqdQ6;K-G&xhjK1{nBlVHc=VrNVR zt|H3yd37*WszCGbvi=srGBE zF!;|Xl;P7(;*W8uu#^+;LOyIrzQ{7siM3p{^*x>0yAq5Yk zlLWt}a@-C0*eByBt&m`2Y7SeAyL)^#>kX6Qa}L&UJiLJH3}!Lu_-XIwp*-)q0;USV z74_V0`gJHaw!7n=9uq69G?_`LrFrZ$Vs2&Fg;%}5jDwxf7iCjY3WigY2af&ktq=X@ z=Z26Az`7K2_ zOKn5cx{Nw6quv1x)cyV6mz&;C?qIlPFb@hl$c@V=1i`xAw_%cOz}ESvR+L_14AY_x`?bq#Fu z*)FxP&GcX8mepr%b?6e zEQ&t!tcZ(CzO3nloE&9b))pl-c<32u;p^S8T*klvM4WAY+Nh>1o|HsRj{D@7Pt%BS zhG5;cJ2oAJCUXwjA_34fbmWd6>%U4-!;n7Hqd%z$MY}7~UN#OpO%+MW8?9^215Ux-;bJt z94bclk6SDo&L7)a^+#6?0{6oKcYE8N7AQvAdE(bFRe#!K0 zdGOa8{}DjwUgD_?>M^+;TrIPG96$jM85JG#KJ(}femUDO?%M6!B< zD9Ndf=5OMfe4j$K9lse*nAC1X#NeULI+%R3{$~8k^s-R8zvP*i1U-y(3PwRWR)gpQ zzrqM7nu^;VZm&Bfks}4HkS~C@1SJAN48b^kqkgfsJFJS+A^nsjC|tU=)+FhSt)0f; z%5@~gX}Vo8Wzm`vk8ARe`TkEdM0B6K;`*mQxZi1kQ(>okGrN*G_+QsUfv76jmIBPh z8gZe76GkSPj3h59=ZTS88t_{xF$p4nDE0r6G4auB6s)jbrHqaIVfj#-hiue;-!-M7 z^?tpIhlKM&G%&+Ij9R^Zn*oV&pA!xawp@y2s9z*V*$5%t&qo zftHoznP*Xr(HpP4OkbgMJw?wWSyim{>;-aka~l6Xt4D<+%Ua$DlYs5SECb6B0qi$4=4gY5rXu!IHtVL5 zDrC{fCl65ZOmc;R^+pO1CyD5V9_AXZVCL{5^je>U#4JX8!_jE&z?1|Bh%-uoly>>+txF49wow#CI~VuG(oy8N2822kQ|%d-fqj zHOb)u&)cn+la|E)q}mT~;i&MtjFap8cQEJ|7qMq+ZbNab(y>D?*p8OVoE|kI zM|o{e7U|QmEB0tiaWYZh@4j(IvkzmdMb{YVrfQS%z`oyu$1C zvC`5E+*|47T-_o7kP&3_QtlJwY_SJ@Nvz4h!MX{_hBKcrQUs+xC4Ry=&9De%lV6@M z&uGjaBnsyf>bk|B#gA(qRQ#v%TLc)!NaU_*pA9ao1)$N0n~3|@dQG^ktxexxa8*r3 z!BzY7cxyR--ouK7F^Z|BXfWG5!8fJ^5k^sw4q94DhdwWW{z$&tR1j!yp~)19_~{Kd z`l=iUBGx3I`b1E-h$13F=w?nyS~~v^{OuZAq3wbE3us@bU&|?aW|sWpFfHwN4^o<^ zO)kd*B9LW3_t^w|9%ivAJbcn_64d0w*ko(+!uWde@pGj0+|(wx5rhPS>)cyxhL4}J zuz6o{SkQdJB7gZLH@e30pnn1csg%~=s_U`w?bzRv`(7Fp!UtRjNxV!ZFbr2}EZ!-I z7n|)5^Ui_$kZ23H1}Ow%>VM-|RV@yd@F8d&1nnMAB4h^GZ$yIcCCZ-7)HUtl-Q1D^Pr!$=%q!3pS_qIQ7T>{dLmDD`ga)&=;UiC0}e&ugk zul)qRVac?)T+F&5^KaMzF@RzpD3MTQA29~2#liEL#!*FZ2+x6p9ntKrVO`4xB{zEZ z%lfJ+B<%ShiJmx{j<&sC8f@} z{1A&#GOyFNkkub+DS3D|>v$f%XY-Gv0-To0XEUo4NQ8tNZqSdz$@h!TIr0S@k^{3# zb6;5I;qX{=PZf8rwW5}^lAN|N-F7FYVI0p>FM#*8|+Fb1ZiMR?kVYwgauWss?5# zaCF|?dGmXr?w1{@w5-BQUIFTObgGxLvk=6>GY^h=zLvMA)B5=dx2Qw(_JD5jR7G`t zXAm-{+u2aP*6haI0TCyTb9Vk^iDzapbmJP*A%G`=&6JI#s)_;lBmcP5PCWsG zN=hO>Y~sc5KGMP@?E`Ra&P~$!m3$V(iS^OCU^x$_%Z->%P=T+F&kg*)`0UWh{x(mO z<9N}o*|+;`KQDriCBF#m9vXHyT^!Bf<=FNt94BNIu$iP%G-f`$(}q}a)1nx7%RKQr zhKrm&uS(0nl#zkl?W?U!&$D|IV=wRg|qzV*;+{Os(JTh~n-R$>~ zZw}Bce<7roF#LUYf4rIPiZUF5o1(k^YY;&}x=|rxB0amw_hAQ5FJ*n&SAcNwnVC>_ zbv4@U>FV8GNz=lWmgRz9pT8YGTy7@F?ojW?2n=1+q#9hW=j3~Ak8v<4rKY?zmQI<*WE?AdkEFZbk(0-)OY{!v7OsyTgXi4}v_B zN8Ju=&dfA3yz?pIg*z>|7Di3u@IQBXvN2*Q9vg=wosjZp2bznVV060QX1lDYyIN4!i@iQ~xFf6JoM? z1S;4wJl^$a+NuenPZtW9F}jXnJt5}W9{kzvng{J4V-k5{*>rbUQH)lFF*oMK7KYiA zLgGO*olwNfMVlRIkP>}u{e1KddhIUbC%WQJ+&iEWosh+1DP3S@{%kDKp-Ug7!JgxB z&DZ$Ks?AWJ4X&SFa&nxSy1{i+_Ev}_C3#@GXoH#x*ZNd%@r{)S=#0C^MY)T zC>}C=%bt>YlZ||TokV4?m69?D{z3Rzmn9I`5VB{1Ihjo%_~?dtSRiFiQp^;NgB8j% zAYM^*;+NyOoz%@B9F1tYg>FZ0xJBPuS9C)D>8lZspY zi1^LJI=!M|Xp%~57h(lnH#syk=38Wc*XN=u!9QA)W^H3b-2uD+zG>n%Jgcu!{^q#x zelZP;DD+gneRXp@^SPEm?$wXlE-GA!j>~%c^(X{w;z7d#qav4}V|gX#utK zI%FVKxY5`Bd_vYM= zTypmHmz;tcO;7^)8`lD4!m{@~do8fUovy$vEm%YYUbhG~_UF%b93F+$Ty@g<69!d0 z>Qs`7hn}J1DrR2)zqkOlcF8m`>dKgBwE7k$dhel+cTXbc*X>%hr#fN@3EX3kOSZx1 zw#5O+h&bj8{bY2qKy&$hzB(z&Nh$)p<~JT0y%rqn#ZK~L4dBCklUsQg0MW7&&$@!^gK+gXl9Yfj(p7fg4dH2^cB{%{;_s@06- z*O*rienQJ60?Iz2o@>lvvz@-XwA(`|YCs?&D2Zr=3)cXY`%GKxF!c=`0mtk@8`-`! zjpH}-(;t|LshFi4YV(qEBtaM!lh~vFM6AA$AMr#UE~7taU99pu}L8d4|m+`PX~qE zIH#uobF!b~9o{YwZJ-`|m3}}QYo56NQ7f3=>-^8ewL9aCEq8RR;W1O*0Qjd&&oKOM zeC$R^>)}wP9)8*-;dOVh3of<4KLQ@TYm2Oy;RrQW&Y)fGUs*Y=);8pz$;oDQwOhXM z%CC@TKWU=0)ax=}C;2}R3?SZos7;{YTuln<^u=y)29te8iJbr~?X8KveBWndwl-{?sslQWK7*qlt5u@w(^E3(>v)S({hoDm#H@E4YiGD zo4emcLYlX2<7yv*Zt?Xz7N(RvCH2f+rNnk3X)Gu_i2R)V60y1}{c$A7)RDc~=ag5_tbX!zQA3)m*sTw!EmXEiQE6HJQGYC2i{d?B*v^SJ!k zg57!;RbBuY>K5)z-c+FKlgc|vnWwd8%n>q|7&HO&$!0-#yE<&Va0Qr!ZkZyHUY`_4 zBWCf2I!*MDqovv+2Yy#qK7BweAXQ!{O!eT7Dmf0PKnqb~KuqYX!vm?(~*3#QN@q3yZk%UXW`>dTOERj6CH}A5nBl`zXsXi0-ne zsnOPE(>IAQiV*aLUuSIMEdIzrJmIV*QwE{dnd#{xWPUht4!Q$z*W`DWJ@(lm4;Hlc zTu7~=Y|%<$m=UFm)cMwvkEA&%QqJ4W`M7|yyP`6;kEM!G4v9MWiU;^^y+gOmBY8@~ z0j&WYUNb$fKLHV&jpeS;9V&A>G0*E<=#p@`>72Kji(Hu#suadP@Ug_piqC2wZXE zHbGCs=Lw>=>H68q9CtflRwK4mr#$cJt#kWxOG+~pxTkO8bf15xsF*c$ECb(&SaMv@ zZut&p@e0C9)(z>?={+`c>rYQ}?-CN-wTIU#zvSry{64DEPa&0abyN+cxM%7WQK zpc*0~oD@jV+6V24S7F<#4mF|uBI&G#4Mi*bXL~$RpfxjGi2CB7iyO2g2qil~1 zBoyz1wmULA4DE+~!^p_%Oh1Sxeh|*ZsNSraDX8r?otTY%MYVF0g*y*TUbhlJ_dnW@ zcu9~jP$X`NPo6LkM28!qx&DVsaP#>Xg$EtddBR|?Y)K*&Pr6)2*LmDvX8K<<0xi$S z6m%^<#3yw0Rpi&I{W)b2efTKY;;09xMqCILhc2Rs{#az}XzD-N1zPTh?kzZ$LDA{K z=_QTT6~)oW&f4H1K{4gI)J~VjcO8>VtE+$&E6}}!{%XZawfcv8F;Hom0u7&4L1%bC_T~(F)6~*v9y6> zEzd4*S?@(GjX#PxmM`b`smjN6$vWOjoJ?spvc+CDq(VMC844Dx-zkDU*8;jG%6SmI z|G{+POYBrUGNpNxg#pyPL-k5*Vbe(dZp0VEjkoe!7?y@_E8{nigxL z%W*{h>+ykPFPZsOU0d6#&g5a;^p?Ys+G6WK^5fX2w{{C7aX93NfoHt+N?n+BpI`r^ z0Z!7$`a6=5hC9p!b{c<7FI_UlxU22tr$QNgo2>vW}PjAygy_ zSGwTj`@4aJMJ;0t#PqlvwU>Ni39R<-#OnWQ9_>i`{rs$?H8Za}SRskk{@*h@SdX?L z^DN!wv#Y)sPD|WupLf~*{3Gee3TaTEZQS%Nlh*y*4O9gKsw%@kqn4~JEETcq=~*pR z8jRfVaLCcw;k|r0)!|diqPHk#pg1-)Zkj&&pBE8<;E{(Qi&h#%*fx+?CYNDIxcdzLUptq5!`0g6M$;&aT>}}o4jv1nkZb;iXwiS{}|wVZ|w!u ze#@F{zvkb;jF7)xHpoGyaBk>h! zo&y^P9vS&AaFxC10s?Y+x`PbRak`a-${HKb4|m)#K5SESda{e&6``FjJ1V?^oh;HB zqdTqpb6P~B33vYmzSi>EDr!>etLJ@p2^aV3%{d)&(MrzVn6?ZfE zGx6Os<(&3pMa6^smgA|yHErm$v_uDR7nJIm4#${W+qo33Z@tojHc+@BrLG2^q7$mC;AUQOtIDxk3)87?iq$1lVbex zV3G*+mf(5sd$A-Yg?K{3!EKH%1RcB9BoY0ZYHkVAwhXJWop~#?y-@#5Sx_>yT9*mg z?{YNQL(e0FB9sC*GA?bt%c~CUVygI;Q^CwL@bU4FTd&q*_Q>xE{cUY7A7{QWGY3Bk z_%u?Kc}6@jkQZx=hu&m`Hdun>n0mAf<-Y#YevvCAm$>G&P}~TxV~wU@mU^qtrmjJc z5iwWau-jaNo0zow6pFcOn>G&F?1|&yL6xG|-E}#}vJ47%ERtRRV!pPntoNT9FmLtz zoNo+eZ9(DBev_I}--T;HOPe*vXgyA`v3+-TdyFHltCZx+Cq=#PM|T}gFw;-P(&D-L%rFq=f4eUem;cIrA$_+1 z&|>GV(PQ2{KHhC>b2Ts9Za+4o@EcB@xTgGsU|@R)38&HZSd(=AZ0xp4twOh5qE}Uy zg$Gwh#e8@u(dpv4gor(q+~K+2NtTfMhvm6FFQ+NFDc@e-^P`e16w?8HW**1fY!eOx zinkKp^@{$Nn6n^qZfT4654#)}UZa^%`{Odp11yBVlp%Ss5mMh-#d`R~Vdbqc(U5ch zprqZCA1QI*m1^rYZU(LAq)f020#5Z<-nq2B^}XAz_q%s1-)>&s_(Lo;|G(82dpx}~ zgBO7?@@&^wQE@P@wfeGuY4Q=_Dm?#(%0J8(?%qF5fouO!eR8TueBcj&ByygIW{2t=tqhs^0Wq{g(!h0W37n}`a4IwX5 zr2C!On*8i&K`0UXkNNVq#F$Bm9}EXB>K5BQdN}z#;P8nXEk+aIU>8lAjoF$CPVRz3i|^ME&|wKd|X)ngGh57F%=;H zMSP0?M_a;MaRfS2Oa1vbM*{IKKe;s^MrD=>#C+vHg;>fB>VFN||Jbcj1UGOvm9&pU z98sH1@%J%l|84L!gN$j_a#+$>{7pv-6kH}=WMe5-9Q#i6>SS3Rd`dJ#G0WRV**V9L zsB)4uBOVHEZ1+J8i&gXUPA5tkapYD(%b&Ej94oPH<2zQfuvEUTRo|>C$hXELc_}Q5 zgEUo`NQy7<_J9U9K~(300|^`3!ZLdU z&x(KmwNM}Z%(i?FZ}|)aLFXcQHG@u4vdQ6TI)}i|2f|u(H>dX!;??&=L8fZBmU2*; ztGGc(8dnGWcmSA6#(ux&akhuDZ0OTV%UDdsZtF`-9=<8@HMG9KM?}CMP9I0znSK(A zOquHIc$UW7nyJf%slV@tu1v#D+Ya(cR_jt+#XSHk0m|L^IQfm~10_!S^7nyN%P8Gt ztu(JX>|gnKoxEQo>1qVE+v?-6uJ~a%Hq@bxHCMapRIiu;l#%m1dAzR$2<5?E@Modx+%5mh=BFWPkjJ(Q`g-YeKj2g8Bb8FSW%X&9k=3+jC$vTLhZH-(MfwVnNnOhRY$Rhe16 z2}S~?1PM91d(B7Ag8cH(TAP+c64LSX&`nr1U~UnKu}GTnHf@)W&=h4m>EnGGWr85Z zbm;16@ly~#m&jR+kMEVrX}~;nZ|PM=Z%v=riOmXD_4aBJXfv_e9pGJVMVJn8>%oXt zCi@-VDo!)}kqrMee=5XgGIPVjiaW3DVsamv@SLlvo6+?2-p;5u#84e!Y8jA+pj!OIX2j#6@^2ZUvqg-LxCwMJzG8geKWjLW~^CyS3 zyXRTQm%6iCG3?VKMH3bCK3Y$%K|L%44mUS`N}`AuQr~3Vj%h*cGqD?M(>w?N6w?sg z34O;uyXOo~=yF{BnUD3|VHz|RZnCzRH7sjx$WwSW$vTv)WTQcgr*z9wQHAbFcEYW|eyiBp567oLU$3tgh6w zq?2Xr}qEVdHQmp904sw=jg<&Y-r>Zb8m zXF+^rcwWCZsxnHq#lmfchi`uj6sniU?zi5xaeGB4#DDFyQqtcw+T0*1Ub!+tDbexX z#*DTlHe=kG)bu+qEfUGgtg$bJVz_~q_2KO9QIKFL8gQZcYaweOq_aSkz0wo$?RO4IK!)>ltyS*~KPM&M<1VRbUgAn@g z$wk(KAQcvVmu2Yw2i)q$n&GrbuI9JOhn^z=$D@(T78Y49EzzC7u-FhQ%ZcAl`9tCD z)uL_tY?@S$Z_n}TB*$e)_-jb~3?e#B@`@tSh;YREspC9Uu*Qk1HK4-7CuFxbM*MGR~!u z%P<~T?m|uJa>AouP#lS_MnoRPReFhMv+THd=6pMjGd|af|4cEBp_bK!qMDg#n%U@8 z;7X_e9dsHS*55>3sD{?XwX`Xn>dv!s#VNK!quRkx{V)`<_kb`Zn>r=(5-KTOv=dety0nS15Dl z=N2~)4J6YOtk5NUYqc~S-wM3?E{6%ALJaJ>v3`Lnw_*pVZ~BP-6eWW|I-b(M^VyS^ zx*89pr+$Kt`B)Ic?v$Yh!pZ$Nwb}pA2%!o7y;5o{lMk}uiUA<(WRajg?V>(`LgwIm z@q-+@mLh}xss8G6{z)sumiQVhk0y!=M0r~pV+cF@)?S2Q4A-C72_9EXxW|w&SW@UVcVjXQ)c;k1;|LU*i2YlvK|Vjf zxF_aFeQ(gN5pMF)JMeS9y}v#_H6CJp%EfgdD4;bKDXgx|jFHGQ77s=Cd*DJ;fWaTv zjV`J7x3_k%X*uw4MXHYN5vJ$sHI9mrTpXqxG(&#|V{iE5FOTXg`WS}?#*K8YU|3xp zr%Mrbfm@TJY61m?&hS|3a=Y*+KYp6%TGCawh7<)VnUnkSN}li zTNNFnsp)a{O+#85)?c!C&8?&a4w=^iUVp9oau<$rbo7Q96CF3%zs)FE)L|GbE(pSi zpk!1(t*F>}vtv74&~cU`jCM7K@#Pi4Zu9jY)(ASMuIr1ri%Zm-Rfj`-OEb4lMNLgu zl$!6&kiCsfKlu8Y!g7eD=k3yj5A0uRZv;CKmeukzEHlxEj?0b4>5bcMRO1$hoah?~ zNDxJQu3!E3J#KQABkonczw1v7?4VUZ$Z$$3dKdfOQ10d=p?jfDYLB2+mx*{2QJp3c zpFv3-$ZDDu7RJ3tI)ng7{($Tw31~Xn#CgAG=g8F|MMToke8uC;plzVlr8v2^ zHf?`zETtg!xF*RN!lwIS-ZQ_q_mjwvA3N8XB^?+H43H^>sRnlDgs3oo*w93TT1EeV zpcD817j)ukiRf?FrBBg+2{$3=3cd6{aa~Xjdoav>wY8^UtF8F7A`8|3rY;8xTJ;=$ z{UOi$a%ZE#uT3wx^T9=1+`oOsdD)&DLrlXz2lF};yKsAcX(?Z_e)YH}WU_F#v-3j! zHV>X45#PKL;CsVC#8Uj>uYU3KU@{0 zBA#Gm_wPUjF)r|~r(lUXw#jz4yV1|vXirYJgn2n8@;UIUmLUh|!>T_H@(@p|7g0NlpFDMaQty}Dwm-&wPeoU;z_Ake!iTeNdj*Ap~{;GYz z_8jl-S=t&=@yz%*4dKL}7~G5F%#hS-mdYOYmui#p_NwxaqLkdo`o^9v^Wi8I>~VpM z%@rS?faQ?<#$884@C%#axqm!AU~-I7JK8^v$FxdYmhvx*!t?lIxFs>1;Rp-iK|GkdB?m!9B} zUdGa_QXsR%d4xo~*6eGmD6FSIUr#EAFWzS`c&F`Yfe#f4z#8qfDjJ-eCBZvV)o04{7FfxHYZ|s z-A!1>)yYYoHTl14JMXY4+BMCCB#{hC4oXHq5RjZjl0XZHh-AqUnw&ETA~|PJ$x6<# z$&z!EbIv(5&`tN2=bPP`vwLP{cjher)^$-`U2j#t&wW4l@1gn1TK_{qh*W}*#!%FW zRry`3@U z%qcSr;)b9dTp3r_wHd}3IevzW3N@U{Me&2r@pk6Or94szo3p+PdCX93><;0~E-!Sh zO+09@_k2R7hWkmot1XN8tLp^iB#E;dZ{UR8>j!I;*-|QQk2`|zMPIO_{oo`Dg(Jc;&=ReKj73b6MA zRA@v;e)Lp231SHt>GC6AFhpsed%)wTa;MYgf)L0_>MVB;DW28|-p9E?tL#|xZ0U7% zB!|h%y1&HS5AyDuZ#$iFIx@sWqOmmb>|fz8s0Z1&d3PN~QO}S)>zq|F>1_mCTd?nX zK!fX~tTr16E`59!mPd!aZeP7w-m&PrSx?o>>ak8II!EY+p5Sa+8r>WFqU54VVXHrG zjv8woN;DC(GTop_x4h#%OCZGp=G#8$W(eXjQt}O+HvAlt)h0M~JY=cICe4 zJJ<^e)xNAybP z*%a97W8^vS=LR)pJaN+|TH#LYbV?7QlC%F(xzCg7xio68no-Y`lAXSPgwShw~ z-*4YThiiE)GWN~%9I#Qf?)OIS){P9`q_R?m1Q!nQVw+B1C>cCUqx;y|G^8}ZBl_#h z&k}Bg?a=q!%R^ipN}9Ud5d;2t7=)P#%aBY-Bw|2jLpZdDdO%hhHfVmKOUs;;Z_G zU3v3oIulWRQQ8J21qE!%qYQb~GcOHF-ttP2C^zDIChx`p2JwIXB5nTXS(i6q#wvb zlP$7R^R==VI-sVw_~+hDf5EuTNWHN!SRUNE9Ku*i_mEf9czh3faJV+)qRLfYn6jY9 zHZ&wtSxI~0tWY>R{pzw*EkAqD?0|B_|4MSeC}78R#$F!Z0P&hG&qP9nYb1AqVr#;d z4o%Nvb7lm8du;Dtu2upL?EJ)Cm%OU-j$eTCGo7BX8;w`n9NcXYH)RM`(z-JG#&>Yo zd5SV2)*EgiFp${LfXFt*$Jftc!ulc}YFk5`Ynhir)%5pAJs+dB4OX3{xP~Mn7^f|2 z0p-3Y-8joFONIQ`PwFdW8NY=$U4!IdqO&k47=YcXQDmje6k28y-j<0;M~Rili$Om; zW5&r5>y$sej!hPj?~hMxJOaD|w!rFpjrPsyuTj~jGd9?<^gr71e;N9%$qOp#yijaNoKX1u2EGlhZwj~`?6`5JpER5#0O!rqiuN1wso7*C&W)SRxzCpWAU0`UO( z+2=yb_=a~+_3Y7Y79U8#skn5pN5q+!&NwuILd~fCo{q2ZKyc?tfmHiz9vOYQgoL)W z?n)K&DI z+H^o^qA>jR@&`zMGI6mDK#5$u?=Oxn(FuQox0iSLZWSteki6~R|slV^}$ zs{3LqUqXja15nC%U{Nqph+ z*RDkL3HF%oCo(CLbUFn0mN9Cqrm|N-K-|#w{RLq&L zkL+QK05in#v67ZX3J|5?mNKmM0Qw!|xlS0o-vri|68fHz>1>}343c;;(<&%TYmd4Eh$X$L6QK;sQaJOi zv$H`!%%3_oHu_tp+I2h8%97XSh3A&aHDY_UpR@f%hv8El2<@G`N^i5^ffMp*KOpgV2#t^*{K3DBl=Y#|D%r5yS70=cmBNV^S) z84Zh(CJ{+s0#2}^BQr{=02D2C<(?btaD8%;TvU=6^zuhv{3av^!_jf$v{+~Nb5E{P z-$?k^$E2}6k&%uJcos1eNCaZFZh7=EjHd%g$uCjkr84|jCO(sZ!hpR_#3@&BZb7^f zPvZ`iGkVj7lV( z(1zSI`P~aKsI%K4snhrbjTolT`skvN&4c{#nAk7l+VjWW-u1eooP5{k&z*+(KV;>d z!}h%VVgz}>C&=HjEvR-Q=8ry#3%;8130&AFRV)m`%E}waB;8zk-$fTk8M~$C>j3k0 z`63wVEZI@ob1kfWp2im3%6->x`HrgF(6eaxzWN>|wTCo=ID0^aQ^6qHQoZ^!QXrBO zxuin}gg=-{(x{6(^3U2Z%1uInBKfuubF|r`)6o3%qx+FaQBG}5I}m+P`5ewef14oo z?$#aUV7q1-KTjyB?ud80JNGSw5@^*S`IZEScUTm`P-2CPzpEcUGuMcnw zW(vW(8rQ>aYZUNjkoSOVf9zgv85=>r+v0xNa@=j*+o@yTM|#7$PUqH@oGnIS*D8T* z_6x%_T9ZpfKh0t2JHOn=x#FJmWR8GTRheP;mHD#F11KSqtt4)~^CyYtBgsKCszOSO zSFsAF)C+g80fDX*AXl|g<(Q|JaM!QBaoHW77wL5rE3#8GN2C06#FR3~ZwhEQ+Q)yL z)O*f9?9bz))K8mTlbxWUFvvS11`3JR(qht`*p81G?u!+vy?d22I?hOObqi{lt5(WS zF419XpP3aofqDoJbOm?v1rGO7&8oy@(Kmo5QOX7~YPr z995L+G&+pTpL!>lM=wN~QiU?L%c9~1TOAVrse;uaviT?RB%32`-hn=@6*8((yyerudEjLnMSEZm-IE1GdE~E-t=iwqW?#&%bq8#&FfXJthPv?qkK#b~7k0HFqwJ8q0T=&4f| z(+u%~RFpR+-E&-l z8xCPE*7JWZhGP0cK3)PEu zMa36nfwt(fkjGJ6Y-(j>>DDW~qSO(@mUzUwVMeeSht0-vuec_1f>*fJnD^nG!d02> z55$$wsc{u)jQuc^r^DE!ZtlvZ0;$5*29+T@6qZB)wc?1DB82;SY`nr~K5jtN<5Dh* zWhxBma?Xeelb&+60*`WjIqlbIpwjRNJqlyL;25dEPTS5@hpwJic~B*zE+rB>B1d;s z0=ju!wBD>FP#=pKt#3}$RdjmFS)`7zlOConhCr!=H2i7{lEMEx#rwR-RJDKCy6o3) zg`wpuQp+n0?0l=WUPldAGpvS*GqfE^=uno0+d7!gD9w?WJyW;;`qg@c*MalFq0w~J zWN>4L&oSsdZ`A2|&qt@Z9r2m&q@G%gH=<2nG)vigro{SDD|Pd@&cXF7r6a~}EFm^$ z_dQYRmj}LwXv17M7K_4(&6PHUzN1|Vev7YVJ||OYYscv9V1G0A;yjkBX*zZqX)vQk zOV3{Dn-$9FnBm8hZ=6rmVEayj;|x#g1RMq}OpYT@ ze;h35uMWeA_wJ7)44Ax-*Blc_AJ!vTq5yNLrffuT#B-gu3%mrN?>K%{*`4V$g~(Ph zk;7jWD;)M!+p~+N$sQaiS*&3$HafWIOr0l#T3P%Loo?^5P!NYiT|x){1A9|p%U?%E z#{+POO<@j+g5aYN`mg32@ovh`nha(J>d$*(wBp=JoQx`F6|cotrGxjhtwqj5IJ_?| z>SQ1*){e;#pt zf9|M~-)dD@(m*THm=e?H38Bp=W;a>#f;(#wZ0w;XQwGeYmz3BKv(tJYLPY9!@)B5P zvllN4Tx|28yph7+oQ9v~TiJ4FX4jJ15R7W>Gr@}2mKI{46d3c{Z`IGD{3N^UauW54 z{1odsna!}s7~Aio1x%r#eJ8}-UYNiW1};q0+S^FBo-=sSw%`KsT$l{P`*8-JS2geu z{lHFaojjs5_RVz7?52-lBoY6oIGv34hUHY%+0W~30`b2G#>!J2F>TXMr|~hKl1FbZ zlnu2Bs8ZL}yn(4P#gj*MA628Xj+F=d%0tqd!*rbK_sr+I$x@JIW~Sr~N`>d_dC>A) zlF?TQJYw--hG}ZwO35%z)OgUCJZPzHj@o%2Z&%;Le>_f>RB0z`HNETd5e>(1DH`4R zz_OdSEpc0|aVq7j@kNwl=naH_AfU<(Qv1Bi@q*AX(l@)uT*!V~bn|jaSLT%(m`2`^ zhOjP^zbsSb8&lWtG~vnavbE)}!>{1C3Zu$J#>&>B0l(OTh7Y+fpx2aMmmI?9ix{CH zEf^D1Fd7aWadho~P~dH@6y87v8H^KoLUYW~5OJ3r=Ce=8N_wBo#+3ccw~$li63@$W zb_~01s;5rf!HUXcn4^74j2iT@xAFTCuQ^mDZPEBK)5+KFbs%*n7pP8^7`kpoc7y$+ zFXGozNvOCs0Dvt5l`zVsnK)AijO}@{G8&>d&#?97;YPI&Znc^MIWhUR+zDCw zgG<_w8=S?BBHJU{E>pR9MxFeUdQO%v>W+Sj(}(rx%0f1w2@#Yp>0BaG0*2v}ZM zY;+&-F`7;I6spUB_byMT__26j%+H_3We;}=O+pwk4C*XI;E*l4T|zR|nuwm*3pY{O zL%#UVtlh4y1ecue?5lREm#aIlAu0Y(<38nhP4iI&?lDrmrn2Aa^xd)|oI8>CB?uOU zTS}K=tE(p`M^(+$<(F$sI*|AELmT_pFMzu_^hK$SqN}_cV>TiUQ#8Mgfy9@L_+IM^8jfPg{Op){i(0JQ9XuYcgLeuMu^67jp3Lri3^ zG%eG$TqS4(Df4`*vjGs0fdB~7x-KMm;=>hH#V-EtX|A%Dgy|23{mROhKPFrFH{OemE}WD@AGhLMCvJ> z_|p%%t2f*6K^D6j?$@h;$gE^+i_>-o9*?xTy60iR97wsy#C*d~0(oLQ(?kI| zB`7S6nf@IOGT4d|Gg_6UGV59J=J&q*^{17DCiN?mplPVE)fu09zpA5VX8E zS?^QKtJN9{_j&6aX{gdrrg789!&P zI&_lkf%I@kIMlCV1C128aTJEZ%Sb`UVh|1sEjqn7aXu&%@Iki$za8io7q_!bG$WhV ztx>LR_=v@&!x(V0m41rk)@3!W`G~dPBh-BRlU7k95x9rRCqpC2wgwKLt4Z(>09o*H z>93${gxpjE!f|(+DZ2MuXvtsOARygN8NkIYE>@O(Z2hn_#mW-hC|bWwf4xeSr*a0> z#F)+$7rFHPdZ)`;Tj658CkT{frW;I_1!TS_81*m%k(MkGGD(yN(Ai;6fLNV=!fi;n zoxv1c00ICP0H(9q!F>SeaDGCL7e~ea?K?WX7e3@>$o)@AyVLE9<}k(8kS*r6qrM!7 zb?OS3zQjLE61UTZUZXAf2&tqzE>q3I*EY;-EGg35j4+uO6bVP0saL-z>~P^RZ|g|> z0Mir-@ovb79n9u&LFT>A!F8}w~HMszL&fp28u)`C6jW)SvBL^i))i;1!c-R9%+Ia726V8T2j?4E*KqM zOgvMx#Spbl>qk~qWnY80n6XRCIyN>5@b|(P2}%85T zfXv0xc6clK{VoPJNLFE^C{ma-mc+q$6%cp|V?J2^xb3QZ_nR;5au0m__0CW@e5#Ztr^b%U!jvlC0BA zndPOnS7>W{(#OW92{cuN(oc3PGaR?_9Ojcr1H#pv53G+X3g|)cym;g2#4`Hp7r^M; zQYqSA@+#AF|5cV89$`J*eOu(X&h$1W!4sE z{$q{fYT)4D7_2yK^Lqtf0Qpfygc9JLzeKlu$jia6PcoSQ!m zv>}XTJO*603r^94GN8$&HyS>yos@{G{5cR4Ksrqn{PXihMXse4X``AAk`-lB*qXVb zh?IRDt~V2&DE(2;Hg4CuP16QXcVIAWYKQFRHp_ zEVsQF>JaNI%@9`1CA=OHVD>ronNu@X5YY4yaddvRA-h~Ny|)X|`1Cf{aP5sUhl}$? zk}#LJxE4^%{(s?DAddgY5S0)ArbfJ?%D39P{|CgW2<%G#AyZp)NDVlw{MX?qh=P_@ zPYn~$PVTAoF7R(M$3J1b3Lsyw=GiOYAY0(~p_3$qQo&ve!g?mZSLmh4wUv~X9b-$M z9i*ljQTmj9PSf34s7NoRYjSu*9wT!2uo9FRR}BF5G7rY7WfZ(63Ga4Mqqo0lW}F;* zb?woIf5>`X9c;FlwB{TR)qTMZy0-o_3maATX7HsooE0cS4d6N3+3xUlO}pR(;ikfX=h*uS{UVtre5O3X8LneF7MJv2&>*r1LZS| zua|2X9wPPOXY!3Sz^Myq9Hbd;&$Hq5LM_)qDibOXEGA4TM@&3Y5uWp6x|UuRc%Q?! zx-l9T6^0m);}qpS`vf<%D*QM8&)>K9+coO}`#dwHQ-(;oc94OK&78`B)YKbfrr7h0 zVf16T^(T?*(1Mifnt6NjM}&HQ0}29=YraQrOV9Yl{e4hyhsyjc*;6x$yajXMWFpnK z3@Ud^hcgm(XoPpR1dA_RRFE(p^wFBSB%fBVB#z~4jd9Dyvd9v#Lj6m=0`67<3$>|o zsWO06r`aOqk$5H0)RiVvSd&<4JHP9`Ezyyw@53~i`!mpr6j*sEX^BPnRRQgo@773o z!H2d3l>3V8m^3l!JL^3H)lP&B@Qfnz7)EiOl6;OK3h*trJ`pd$tA%SWeBI+6(S<3s z^E4M{yXAnY$JqxHASSQdqM3}S;4OSfYjS6l)p%@H*2Z`p`kn6Xl3&d2*V5aAuAxe;z+`?nfRl>(@|Tw%9hzqA zjoAVV*4anNhgnkLJ|Kwnw+2!-bzjdWX^y<8N~UXF+SAyd#%?QF!sxp4*h?crzxj>t z5%~FJm4aN+RwSITV@vG@73HSzL#+&&0$!pTsQGD=rX_Q*4^w%Q*8EbV!98|_#kVv^ zag4wlK-`pI&b}A0$c9voJea>)rO9EmUr0fKJ8qVv5p;FD3<6!!vm)Vqv>qNQW9%jS zLZre_=_{&&`16KYs5nBHdtWd{{dt(Q4@L<3EYMABTn7^raweE7pXbQ%dv>!u+z}yP zVc~A+ZpHn*HY)gYob_nxCf`T!+zI{X?V*mORaO87Gq4rSi~mx7^UsSEIBimZHNvAD zNgP{R3W*B=mP6%yt8xzxgX0P4*XIF{3H!@uZicS9A zDeEHmzO_KPB1$*-8D?o9l^fQJmy_)xn; zyFK&9sGISBZ*lLgwtjOl$>ZP!e&d^t`T?vmHE_hnbBF z&QSj~9NV^FGaDSRC_g+%#Yb8@YwsS4!ip;|!{ev!PTl{!bTbUB$E)JOTn=yGfsx(4 z`=R^+U4ngr89FBqJbGWss*b^S0I!KUfC{fsoL#k^P~jaCyLhO+Fs^IaNL4(b91uE)7j{9)&N9rJ+cu<*a=kA@y+!b zh4GcmdkC%TQG!4G+Qb9ADtmoZ2Ot*-eSc|p_9w0+8b+@6NF@cCdU)(zk#Y`5U3@Be&HE{*4wSTorJIcp##uKB_GkTqs7rlmgqa*!lGECMx^KU5cpfM3UYlKitm>SFaUZOLTT*$fxLd3 z&*`h{L-6{%g!`Kgl9hHG<6GzYBT)}ufZTXlHn2YRPDjw8MeCPoI_JB#I2O_rFof!O zxzbDn`+dc|BM1#Z)+A&uQhsp0{rY-qdqA+PUUu#a7BYxPnRt4#9puZnujkuvu@9gn zZ)m`6oMmmQLV0#&tw(mV+7%2Jw$|}+3i@;lPDF^4Pz_d(y`pwxO*j8Y+hN^n1||}b z-tLQKuyT<){~1a31g_LI`hr(khQUY%ZZ=9BK!n!oby_}dKijk)7CCdjbp{yn_fCG( zgw_r2O_F}BXv=(#K}`)r7go}jBkG?QM zzoMw|ym4}T8?Z1R|0)FDc9^uSrR69ukO0UXY7$!Xb+!GiEmpr_1;92z@tCa)pdFBN z2f8iAmV56!nJJf%k7K$4iTQXUDted-MnZ{*{8HyWhzK*MiR&DRi9TjipdU#~G)PfN zOF%RiD$2(^E`SpgS@@4@P>m64zYJPvy&$cw8{l?z9i3o$@<9hTS)cWZ{ySE)1SWL@ zdLoM$ALxSkg(w-@Zw3pCffgY1s-S<{s+97-`{NZK?AFd_cCYP|c8};ONh>x@WImZy z%#zU2X_(LpsaUFiqQY> zD#B31f3G4;&_Lxyh<^iGIof6#G`GlrPz%LsfB%O3adFhNVKN|WL_)i^DRY>hp6e@gURuj&2GfiSh76$t6F-IaRj8?ZH?$fQ!_jJ%(^z z?tI_aQvRl`Juv%wFja#L0yAj%^1+aYf@=C78sN%BC=GdoHA`^vubVKZFPu-AGd|`( z@?NisEr%H%G+lqhwbgB12Zqn^dFH~_CSeeYvL{3qsen119s$gfG4wssOLZWgXqA%% ztGWPhfwAa~o7+nac+w{cQu*cCS-zkR?bf(M0KdEAHb8EVU#UAHWk~o@Xp!0FRDnKr z*9x}GGeK;U#1#wBq!Jy~uQ&~3r-9sq;8BCZ*0+LBVD=)(cw>B|PUtP-`5i1B=v$!N z0&tZl6+b_jTHkt;j*E!!_0+anKK$@uqr;#WleEezUWG@uN*)>H=1N$s4}91Z`}gvr zA|g!bO5+nuu%g@SC<2;|wrWQ+?=2-ipy|72KAval##@7&de?%oSRxM^_N$UKYKQUr zbo#CAyNDsdX1tL9Wo&~7=&X2!Ui9JPmudGC?oag<$In*b820e48U9eZ(1APU9&+TV zx$oj!#P;^bfqxLVc#3D3Mb#M**MgJBd949T#47D?{x;tD-^}fQ08IRstodJubB@)uCLIoe zta*_^V&XTZKgh#>7Q~R9VCP{Vohs%BB4c?bv|w-UGjUSMC16*ODfh?Y_LsJx19W@8 z_Hq+9)0JNKHeNNvAb3&z)pP(|!f?yi!RG~U>X250iYZE{+nV*o7l6~sT;gvxSCjSz z6fPciM<0$$SeG2wM-i_hZ2Pdu{a{l~GJSZCj@A>7WkX0Y_9iBfio$3Hu*I!`0$lL} zy6wdhn%=AlE{^CI%%(4xqE-t?TC$Nlw*$N5ovRDVvxiX@-7CL0SD8~)nQJN zrA43`H3b}ki3DAF1FSrLJG5^lTdH$cU^FrWL^KeGX2)&`E59$L3Q5q!N#pNdmX!}O zxJP#ObSqxV@u_}f_PF}_v-aUmxlVK&SD_nCidcSQK{S1W>~RQ6A`sFk_Y&QPPzY;+ zJXTXMJc$mK+vFhh49vk&Rr;9vOtW>DM~>-<-V&qo2qZ2DETPIgs(Q3B607M;o==sB zENLaibq=zM)6tMwO{E~+v)dlk9YKF1y8WQ?w<-&C_V@QXLx0PM{)maEeb2;f+a!y8 z4nZlgiin87>)GxZEbaA+tyS>P0+)I7dqa`IP2MvSsiujld6rk=HqVIAa18G7VzcZV z3x=4FFJ`Z4h{nrL8PGP=#DbcsEXaSI;(+Rz%XIOew_=`&A zWFsmEe)5da@8X{Ma^b>bYFyG&xarMreE~b43LaB@{6YJe@>UH0^49n_7I@gMXdhgQ z*+zZutFVjg^PvSE63BJR{Aj1Qs`8~k=6|pn4YqgvE*>R+-&Mc_ZR`!2|CRnRsg3m> zJR|m-)aEu(tze*4?>6r)Ts$AwoLR1Zz9&FCJBfm~jrYs7*Urq%lkZXQe)}Os(SYwJ z<@@8+Beg-x;E+hvW9U@iP`u1=0u@kx;28P^NNbuLBfr?TVogL`LF|~ZoQtK#$_+N& z?#adZN~beNlE0KF@#%b#&O`=6GTRKRFt*z@QcLaP?UeooJy(s72<)3#H245RyyC+H zuH=K>YQB_+NNI8UlwKh1cZj2%nL$&$f-4}UFWZ~CRj3x(DQ+~U#7b*S|>7etZ z$QCOCGsAi+Z3B`_xSIxQWby(!O?q_D7MqEYKU;TKYkN#)6CX62f|dNyscHA;EYlO4 zKv=>Ady3Dq6k@uaY2$$uq`Zp4G3^MNHt2s6eBXBjNxCU zkyj?~0Z ze*WaF-)ZO-))x@QRht;i=kZYz41^5|dLme8GmBJ|{zI!>iP{S=-@+U$UVDlp;_ zsN=^Icxggq{>8mUH)hH!w^0MQYsWse2-)`9V1jh-Gl`%n2^zHcqx?nSsUKWgH8vl6 z$oQR}>iI__*arTL3Nv(~sX2hjWCdP1v_#^}F3tP!fF?jbP(oOI_Vnl~DnZxHwnISm z3?z{~jFH{DkIS$A@$i)_`kY%!s8!lVrfBHZ9#JTzRKbkIUC8G5VM1P2gen2Fm4tJb z(kFZ5pv0l47O2Q}Xih`#B!D#5NVVF7D>86f4`P2?=T+5AFWWG)yTk4QMKSh);w3?I{X7 z_)+4{bz_kcxfnk-?JGdpwdF`KhGWuO8KKcXHx7Rn#_>{B`yKAhqwrqN^ia2h;#~Wv z(TLot2& zj(gxtX3=p~t!g*W-SI`rtJS9>sGVptW? zn=SG91(`ybgBn;8E%ptun+z_HERENfd;~JaH5U#xj%MIgonST@#AMZTr5m;pakBI{ zwjET&ED)+IyG!Cuf9}PD`0P!0s_rf;ChGrguggV_X$KgV%=ZA)zGZI`5@h7Rm>9$c zmR*K>#BYsX^jXUVNkCgJ;SlLlHR13dRLL>88}fuDWA!K-`Fcw& zU8!f7=s9np*P#w?`%xeB_(UXUYf9xWJL>XU<2p@VxH$DxtGvL&TzH#rL0=Z;etbbTAEhfhU^pV9~D2x`#XLC>riY-;}n?0n#I=-H5v0`gpANgU_ zFME?@Q2fp&&^TGs?KrrqH@lX&^JtHbq~B8b$`_?x!p}+MhJ3Fwk!!O2Znt1)Y}yWK zm3Ruqyo%zD3|=IraYQqp7mX z?aq3Inh*&Pr1RV3HhW=8w_PpMsPvh6pQkVf&`ygi889?!yg_p4N-K$n>%M z-%DAkr989VfM~FJ9gx!%O&W#^UopLSbDr3^77gTkOObYLDc&K&7^xqS;$9e4iRIv6<#QDm`NzS_-3-4Rwbghq56}#4zXZd3{126nnO2w6Tcnr z*MIL|c(59@4(ujPUEX5M9ZgMufmBDbf!2S8`uD#KcCVGP?-}+}Wua2YQ+L=$XD#i{ zmqYD`T>Vs21cVKga}$60_G%xo z=L26@lgYG!W<0ICsF=c^U_D@kqtiup@c5zwQ#`U!_`vAMXi1>0{z$=E8H9qjY+uvAe z*?$Iz2b+*ejX8iD?*WkhX0ZWM;OVrw=SMcRdKvVAp`<4RIHfLOurs zf}pzOot)^6maHX)1z=<$AcAZ(QMALTzLq#TCf) z4@}3w_8?MoG$P6Sz&N~b7z_VSoWT6x@j7haO7@8d>ZoaTQacY3|1%eWGIHtNzqzR# z;w+63gyf8gUZA02?kma3yuIWupU(qxYWG9neIV0JVBDUs#Mu{dzjXp!)W0>&y(Xtv zaq9nOn&aO7eCobe1Vu$v_yqx$yN}D|q~n|7q5$c~5ItV@vP-0YiN+`A2*gOFXy!=} z12BfAfA?}Q_2gz`;L98kF2May!v!f3$|iqB3sm=Xsy2L&7QTZ}6kK2G_}xk|b!2Ud9{d2a?zr_pKf#EfPO>08O!s#-Y3xjhl^#=-c@I)BLH0L(tU3%=$_(ERX}X>RqCC7_NO1YWr|bPp4HdNRv#a;{bmFy{{R zuM8N{-)t+s{Yx)-$XSGM{yXCPvbHkaqg_-x^1!D4M79AKx>_@~wH2!$Qwg}|$0&a2 zOYVR1&)KHFmOQ0Sghvsccp5JV&nDL`Fpj{(<<|Du^N<5HnLIyDVMs^iweI3BYO}Td zu%ExyqnSvkNGQk&{ZhX^C@Nt6S(XfMZf=Lu;9Vkdf?~g1K5>Ans%lhHMhqo+uds8( z!gcLpsgGTz+w+n;nmtZ2hJHtC90N$=P5+B7vY(Y%=QUq%KICy8P7bN``4)j+YIpc*!**`e z+!S`UHzWU}XAt+DIQim>QzU%cPE>~OSKpEwLo+oaH|L--FnEy;e z3CKpieb%w)?d09?pGOAT=iWo~ZfVnlT1@*^_+=6_toR4dhvQdGv?hC}E^e4}og5$k z*>#Q;X54y;pOPvL=Bx6x*bGtQ{vI9#A{zdC&G>*-_bzxz)f9@{)QccZLcAj{PYFj?G^hZNc+~A<6Dikal zUfydDG?`8XnZRu_JVsnbP3r4-*r~v)7U%cgsZT2W>N9t>@lMj%fB){~gl?y2VAwr3 zNEnyX=b~t|DRywTcY?<)-N?hlXqY)rp66xn>N{60+tgCdWupaMIK*Da@p)GfqI^JBjL45|EJ0yuzM*6*B7ojg1?^H#yIoDcTZemkGE z`IE6wutp6ySC({djK+p)=Ml7`kQ10Mcw9~rj1RsYf30&^UtmcV+g=?}in!$TRx3V^ zFC<>DlafnxNxQ*u=?m83ebjvf0`(8vL9yc@xFwg`4W%en8CgHIytQMYjLBJwM}@8B8tT9Sc)gs&HsjPCq<{qLv}!vX41 zqj$*a)k8-O<~^4gzQFnRnpbkBZ}vV9I33R1!4aY~WsV)LHAGB3a#YW!ZrWBSrtyUw zZu+S%uA5rhml|K7jyN{!d{r1VI;^wd#2`sVZK%DtfWTc+IB|dI6aLBgh#ycYs(ahE z&yEtCJ=~RF9CE47ROr_P>$rwLpA=oUwyc-?3e|n{OLZ-+!9 zIAG6vu59g24hSsz zuF*sNgNXAtUPQ<9tBhVqS#e20+_vP0JRfDn5{jk?zyz#*D+;rf4xete>N_-_3gG3O zM!n_{jTW*9$EI}2yf;q1s*nvHtEFf*Y@&ZZLbbLorQ_!N_H?k;ZRPwrIj-ld^A<6_ zS^>}R3I2{k3HaKfa>3l>tsvskB~+m;s1wQFEToa7IxQ zs);5l=YDyaQ)`yb!yI&KN=xaegDia6wRhu}m9t5mkU;(0S@ou8eGH?nCTk4wC+_7+ ze7??W%YDV`_vc5&ux59!pINFm9J%a|T*r6b%N)MxTTqJN44AYuP>^;N88AHcZs@!~ z;+1KY-%TC+>Efp%?dl^>k*KjB&Q`F2-(Zo7#2k)p`5_squ6e|<>1ZsVhau!n>>CzE zCq$K8cev?0(wsrolr<(W9Kl7(eorBq{_|Fhuz?dLT6|i2+)>d1JRj^XP)L9|73n6x z^_}i(xQz5e%F<2k}VcSB3MaD$cVpP`0NS+Ea}JF=tSnh|}*u%2XF z80B1}CE?pcCYj1`TO{#4p`qUNfkag~QrkJzy`4k4&OKs-pWltL(3WuE<7WSMsL5_| zQVLm!#D4$&oEe#-V>lqOh{tE!-?T|(?cP>4q;sV~KAn87D4aZG!_jy_7XdIaR-fDf z@BrS*N$mgPgm3kiH_^MNcm%GM74If!&m>gIFG zjYUw@`$nLr+gvd~3PICTE9#`ZtYJ=j)V0;|<42R=0P?!AFXPMTg!e=CUB&T|>EAk5+((>sQzyp{sL9 z638TUXJC8tM(bpO^wi4w6iEjpA3Ik9*LySszm&zj#YP=5cq&jO%rv4eqoj|SMo*XH zyUww5Bq=PqnR}%A-G;^dI|J$w=EQiI&>?ChT4Y2K=qK(Vp)goGwzUwP6Wk!&y;7>r zmJK-6-;?xCaj4fKJnDu-Djo>6D51oGu-X3qT8r zKB88Om$`QC#l>yP#Gd=jcx`tGV=^*6UKe^jKTI42zgKzfUtD+5n7kIrCsFQr?)8aj zhfvwFaf^YMrfnBGGb&=`y>;es(-UOPOkS#}M0Z>@9!rv_s)@$yA4&=%cu%Mgl@!FL zOZWA=Y56^)B_GVcPR~?yaYIg{mxF>RzA7r@J3^cfy>E@AkgW(hR`2o%vmw!Z)jqo% zm3b!$(>_=H1gV49v^R9jg3eT9XYPMH@w0p3&z2pGj8tfc5SvQspxlY{wgp33;zw7vwmffta55(RocRd zuW*uv+Y|Q(d z4Na!9p9tfbO4>EFEm5gpi$@L}LS1VMmbY7$rB?8_K*`0rJ&Z@7p>c?uYaR-0-kw!U z2V|d~+*p^C{#sd9_SATdqcCJN?TyA*=(os9nDI_YH|*?QcA#zUxDtAq?eLb@PKLhG+y*^A$0a8 zxFVI{zDVzOr*IWxs>eG}AZkjR$fwTd(@K7Rg6HeT6JW==v*78>{LQLRvc_7*F{)J~ z3AxX!{;0rIgP?hXV1}HKp+syYMC`~?X3x#&E;wIE^67$UxJSE}*J(HEnxQW)Lr;do zI~|>q3QeY~_Pys=Ix>+L-q&4M^HS~&$|MWh;W7BE0Zf_&sYh;ikkbRh-jd>;?XlBh z@$SdF)BUn38VWJe%9fX^!Yacag;CRJ9Bptc+V9kH%*?uGN;K%_E_8=Imf=5tu5zF# z*P(M`*Jr-i28CB~dP3fK*@6*nR(1^8*)Xq9!a-t*_ZFi8Bo0aDaPCJzaulJ!j-VvG zTL{NSeymw5NKDu*|Loj|I}bf z#fZb;y9+9jWhl|uce)2)xw2Tku0yD4%-(S)YoQj&%8BI7*>c|fibr0HkZ1Z2>LInp zlvGqq%=1P2@E`;<?0Fz|D%r^}XV!15rnb zAet^I?CziHj%R)6&k-!^EeBg}ACi;o+Lu{?gF>n7p@J^7Mt=7toxT@4Qc97dofoQ} zP+!3~Bqb*0Y}RgmI+-~^m!g>s%v)?x+Q6otFOJ)_Q_{Jl7O#Hcp8Jgvd!C4<3G;jU z+VS!Ycbu35{?$;N0`R!0Y!FFws$XBWHO>-?8)P%69zI?|>!u&ClrtFv zZJ~Z%&_06Ka3^iJD3}S>QsF&!JE%aLUoQkV7g|1yQ^U)n;9 z;7&s9^7TpA;AAQqeL69&=iTQN2jz*oxRi*@XAINt}w*oHDiC5RV9@n=bV z_|0K%i9(Q-DpsaT4AS1HvO@l)^v4u&f#AdZh=_Ib$Otp<9N&+CHC|q42`g_(a*+k7 zkeER>uLR4iX&%(b-$x|VDNICFMBO?6&6|~0%Gqn#pT@?|(`S`Q*0#13NNo^(JNs5U z8G8`fgpCc|fQV7R;fUA8qu|b*9y^qK4K-CsL%_Y&r&20h(V^Y2^8|>g_!M$APWGX# zy{JXVKfqn!@#0`Vh$A(J!x~Mb+X(7MR5L%z4F*q**ited_ zgBmI`b?&V2+4YwNx`?Bh*`SLYJdqml){4i*MmU|y#VF5nlGM^YKo|O4EehxBx{WA6U}KhZMBM?{65_(j zoB;A3Nm(^a)hNw6zR9)uvh~IHfa@0+{5rF3FO&rsqHBLsuf}{bG^)>6UV_a-jIL`l|oA!b1bHZp_E_lUq+B z$@4*)nvO$TBArI-L<~vv?X5%zC7(y-R$q?iyRdH2Q;Bt%MnRXY^;+9>y`=X0FX>`# zr87<-QEn8`Prg}b4Ju)iXAP=pluOsCok%fJd$vho+gZu`ynpsrR|*%G)W&WI;}#b4woKJMNd|g%2U-QqL=jXQf5y4W zWW?zj^XGU5yY!-Repc2~6Mg-xg3y*$OtRVe3}sEMQPpBSq$9>$*xZ*>#kyof&5E0F zTE$cIt79R+fK89dMDC4^JYU_);=txMJy|2z8oT&sX`UH!8gOfiGQRDUo?gtmw)8kS zfHlKQ)sV^U)8)8K&v8U#qQm!k6&BaV(fGM#jHL#J6EVTh9k~X5xqStntuZ6>HjGn`TK_ZFL=shCn}7(3~SW=dElF;6_xx2X(*2*{~It94rcm8 zY_R`O8s_C2X8xP6eA2KY*!QveQ&_I_@1UE1t8M@`3I4gQi&8Zmg2RQ0<7i2w!YOE%H+ZBz~m7pc?LZx5t zX(K4rgzNFjyLn|yE7=!p-*WT8*N%LK_!*)yNa(1;*s8!(2%s&1!=x{=4uX&8B7$qu zA$Alb;R^h^3p)rqDmNxt$i1m(?KQ@!j(PyE!7nsF+Y{T|#KrrvKZP3}vba>OOGUVM zV07JD`}cA7eEmj&Az`xrDJJ6{gWVD_L%bOtmyqii<@E#UrmmR7h^bbp1HGV|NbkTi zO7DwalI~s_Tq3S(A;Zt%?LqCrhUfP9aC?aKi3xn;+Vo72-3j+Z4aF;oukbp6HgzXN zB9CJmxVImV%trjTjng=vGa(@|zot!q>t&4~biJt;GCa8F@bpkrXFhWDvW7==F% zVZ!S*{j(QF6ArteZ)#%9RoysFAq<$vly+Ta!`S789YV3s;2LsEB}86G23Nqd)Hofx zlhyk9Ml}W@_w>zkD=_sI@a$>gYB%dH8x3VM-r&kRA}>tZN2W5MeY+&$ZIB)~y_kxf z#OFzDWC9eI_%H;3BQEt&*R4k8Q~+hJTC?0^3KA$Yf?IfSY})qf4dH#UkB{gMp~1k>yo!O3{9{$LKlp;Kg<)qSVrl*aA8U|{%*0GNH4 zMeAjN+X3iZy5L>ZkFsGoNHf%QGa`#A3if$gB?TeB3&z3qJ5lm-{l(cs==D3xr~Q6j z3*TQ=<&HlsOIp=!|G?L%t0C1@Vo)Ik6)JKIxQ>!noxv#j$(*5wbCX_=wpMsVwub>_G% z28gr$HVFrV#GC9}$pBJPBR}57Sc|UJ7uGWsbo5wc#LVnGuhp(&XTG5-akb|7O?xAW zIYIqZSmWQ+kT(0_YedknatEdjh6Gp_Ero6KEtDygOvO&BorFv$gfN`2rK5tvW)wt1v1^^-2(ceU=Su-j3-*?Y9M)pb zNqx4Tp{RP}^d_6AVKrq*8q8)?SGmE~<%`{o6!NP#A?VO);{f(CD8I}SRMKTy^7Jyz z^ffn;-*2*C#W8t(21D5@u^I+VEp9w}!nXKQ1NxwBHnsPeHymrOxVzKh;Tr=_F@tGu zVas{-MVa{Y``V=&ndHHrcwtd@Z=Pz#^EvgTl*r22v(GEs%e@5TYZ3~4AtwTh z58&e?XLB@@^6uGlD}o?h;}ES>`#6aMcasHbD|^`&?E+_VCoEf@%?_R8hN*q zrHF!c_XFM$Ai4kksZOA@8r&m#5qln85fgK?W1X}&GWYA(6$2mDh@qhg-`O{CdvgYC zd?~?pvv9c^D!W09yY2dl%`ZG*`Vw|v?EF# zv%(x#CyG(mSE!sxnZrZ9#X`>>|CZjp`B-l0p;E+6o5~lcW^hU}GUdar7o2oofrn)+ zY{#qGtH%q6?HUHZ6`Xdlum78U{NLZDXKUf~bDJePBDVI=0Pas?tshkgZ&3$(4M&`65W;Iwoh!(be ziM|2y#}cX9f^^?s!H(U{&8Ll>zW!IqsUv~K9UV7wd$Usr=H~0};SD~TxFSz=k4u6V z%C~o*jk@Xe)|0M{IEu)jp+H_6gjDa}Xib+>24JMZ^2!Hs{(bk}VCzz;%6Vb-)mUBffQq{js`ZcR3`5oVIH^%tAE%n>&&P+US^qa1A z(m3HwOxC;L<&_fGc2MqY$-OaDMc+_U$GAo&->gSF6UVdu8crDUM*!o~EC~sko3 zk!HI1;SHs@VbH}Ge{U$keX*s*a4o+bx4)RkBWK%_yOCg_A9YL4@7-y+>@{D4(z4)G zCgy}kR~s*{i3v*Jkvk(}T$k$*-RbQnY>XUKaJQG4uX-Xr-)pCd>IMzLNXne9^Zub5 z?C>NP5KbPXp1=M(67uv$yUT5U=%AFDX$jD*Z4eKmCof&L<+~m)HT@s9L8+Qgg-RRa z!W&OF5Q*2Ug`8eBNNY)WpLa&nHNyGV#V_NKe*d6ux-s}{(T`z0kE=HLz-zPKeq(gJ z41*%G3ycrLOEeXgDM@nbILj{B%WkO!3I;x0w>tuM-$}m$I}NY^$jC>CcV$3Dj@!KO zE=U66dfwqG8^|&n?A08F@%h9w_DP`9g>hB|Mn~kNyN()rpKeC>t`@4#!`)ZC5{`Fc zLeU|Y__U6d25j)BH6tw_E&5)fnXCo~TVN!mC}N+4kvzIZk+DdETwHYeV^ep&;RfH3 zwyO${8G?R&dp&r4l6qt4LnXO6E^ItOna=0RnCb6#+9Dej#}iYb3t>etS`k9)s_?GT z)~7?I3YP*Lp%V-f4Q&l2;5VHsmY^@r&%ddzuCl_B9%Abs982c;VN_H5WT|ko$^u_& z$MMvSYP==yrT>8r%C4+=?dIZQ6eC3a4og->=3I>Gw7*w!ESv4Y_wKwmCrD%1aFbp)GoD}+Y z={QZmDXL{ahi{?W(ucyr^EPL?nVG>}Z}*8ATFJKzsN;fiL9P}>1?m85mJy0f$!m2P znVpgHPgG3tozQmQ_hf(2!KzEXkwZYo{g;L6UU6tYNvnS#2j9o>SI>_-T}kO0;_!KE ziJW5O;{sCT4B3p$lMxZ)-UW7EF3lBd85Z*+Z}}Rq+g8t4CBB%iuA19bAa%eani-B~ z5VU#hIfsgUq1N*9nquj=vk{zs4T|MWnrl<43s1aT2 zaeG7VjGDxWk9yyO+(+%5oVk1K+N;aC^NS~-KKmX{aglqwvU>y66h$VUVKMZ4MxBwB_cW(ame24<>>gt&YtZY0hOq=&)RyR@cV=%4-yfYpB4XK zT7YRM-}Z_;NeyDlY@$V(mb$mi0f)Yx&@4~y6$;;(dO%Mv9q4J%=J~N6_9a%ZCs48E zyB3-?`*DG~32y8W!WEcl4V<$j|4v42ljTWp)urYS{L}c(Z>PeBx)aKtwMF57><#!%AEU}T4q0ss=V9K6mu_D>c zG9qlrZR;3ETB7gW_0-j_NZ7f%KrDt{RNnSdm_gY-zZUQX*If7VwYwR5Wa&wN z*k|bBWKc;sHnq3ENhvQ&q|8cVG__rFZq2LFU_Y!M{Ac*={kHJ>=IXf zoon=VTX%oNXrjR4cFeqDfg}w@GV*M^@X*#`MW(CiqJLl3@o%w#f9OE&{Qn?_q{6?* z1M4uSil+A;If~?B@P8vH!K&5#qckD^qb|{EI{9FKjAQpzLy;*qlXT**!qoq7$;tnq zZlNA|%J1E^$yCOPmAg(=c!N@+W98|3@K>yTNk+6!y}&jY3WT7jZhms-C+LD3ZgPyt zHWn0jZ}Kx9pKWmy7f^)0S~dOb`@PdA-IMn~QpHOZC9Ub?*J~f|7}uPBG-L5}>n;h0!ak zuO#Swz=unQ4~qE?`mB9jT34(h++{_Oo1y1+2wXcB?(!tyQIX0HT&O}q86flh>OOyd z)7JXa5zl3=%aFSo9TlM%95ixdE8aW*stQLogso;8AjWz}kdq3e9>yQ5FijzMZilH3 zQsNNbdIl*loT$*q6HzuEzHy(oKSg#7csbywjC*dwf2#4^^D%ImlAbjXJ$xCZ%ReG} zyDY*(M2V*PCLSGo3OAjj4=@6%XKZ@M>qXS812@p8~G=D=!s44JWx zQWYwh#rxdRqdr>p@El}Kj1fUL7n+g=Lx*RdODIaR3k9$yy?ENJWB9l?2jYpuu>xLt z!?WCovl>@^z*^wMpfAWGs9QyhUQ>rKo>ho&acAL9TR7`&GbW_{7u?Xg&qXa-XOOhg1z;_i$?@q4r~NTA|@ zPzKmXU9bn(-?eIdgO}PIK+nU zv_Q#mT2HLS5j!C-(=}>C^CAXoyYDD0=;X$qdcGAP4Q-Goa!1QrMsvt;3fRA8x#7rf z;-bJ!)OauuWAhD7C|XIQFK6-#+|&da>*bWd`Dk9@pdLNwLb8{(b48baD+pdj6TNs*-a}IMrPj6e4{^C(mmV zl)~$GtIR&xH&|_os`u&2${C6?RI=lYh2<3kf>zTZFXaMHq}Obd2a?HM2lNkl`~35( zj7gkum`HN~>^UhNza~P^1ja00?l+!(>^5Pi=xAD*sCOv)sDQng*lO?2UtPa%x~D=6 z8?eM3Wp|_VENg33S&|h&+gwA5B`zQ9iQgK>88&_^-PXXsh4tG}hCM=wdSG{rgEGGF z(0TjGrr=jX6d&yiy5yMG%o$20u1U@l2EY0~&wG5=thBCNhls}Bj#G~BdP{1H!y94= z@z@I>N8i0|CG*j1NLW7qpnT$wGf=o?6{Ax^@e|f1G^wTOxc#Se)tyHccY^eNoPwgL~`sBh3j`DJk7$BvF5qWaqJK>&=Y6 zQgVJ0tf9=LeWY~+_t$dA-5xs8#PK_eDZ^NZuKG&697mtu1=r4bG@s^A@e6o&nd=j3 z9)TpG&bud!R)afZ$K9D3nZ(;8Eo}fmkG3Z*px}fH!B+Iyndvz!UAaa z370Q-A|VP;`Qqm%sBOa9Mt6f@_RGx0V2-U#zbftIix>oYPQdNGD=HmMH?dy+dwL4; zi~<*LT%DQY#*;(w=rN&AG*F}r*@}u3T8@&nWP8}}MDC?5LvLQ2zY$dot}S{x6+pzm z{Z0K^A(~hMFe7m6UG)CAk6OO7nJYZRg*qbXuklD~69L^$cc|J} z&#*0d#KzL5F^V&64MgS3S^DCY=zFzxdN@wf{QGBN_8z^{22+&0NW4liMT1VBg0q(Y zeIEug#&^A|!-}P;J^CgUf)hM3I;WKooNo-I&b5iu(^gLW(;OBrO4JeMqxV4`yoMSJ zX>zun)SVCcng6;TS`yVTK4uT1hZ#2rP)C0m`;m-H6xvmJaKGDuzXYl#teakGZ3pZtb8 zZ&uZ68T11!NnLAO(E>Cd3}k(k=rUNuDVeyl7pzb_>K43eWh(6lTMh+f!8hoZM#uaO}gGpw5@sW^zaKczoLz zzq94igk9m|B{^>0M6h&%Yc<4uV6D*>jPNvz^bpWn()7#M$GR_a!)|1=!2MQO}(N0HZ`M06fma(>$2mAf34=~}P=n)rK zqdx4VH&xNj!SZUGyYARAeKz<^nABbvg_F`9sg@gfFy-EeISvQw9!?c&U2_+vC)1jx zWNW9!s)kFpFS^X|gm}V1pM!xv)U*r@(>ZA}(NPV@phsS5L1FrDoA<4q_&r|bx{=mt zh+BwKpxQNft+~4|S%*dQpga!Wh>7ZSNWfO|AY%!wPpq(z2!2HQ*yD36AY(-Zw%Viv zci7xWw1gFZUSI8EuBGZPM%48=DqO!a7=_XlU$=)0`ErPDVGFs@^OkmUpz7~2q7moP z*v5|%0dS$KP`phIRY+Qaf~5Z;<1+pb-RDkY^T~@*Qi0fo)7#vGw^eujuvuJoNS|CY1u`- zaX)nAj{FK`NF>i9ay6)JWm?aO$V+g4lkjRXeBm@Cw-Y+K)Yh4cio*BwLAcOQ#M$if z!{DFzc*giQ+Qi}!EFS%zi=qEHGA*@)Z)w0+RLrfm5&v#xo#TT568A6Aooqe7Fdlkc zwG=xFBLU=SY!VKONVOK-pDnwQ3A*a6t2~{6{Kr*HtRdo1!PBmURMdlciq)&_?Vh#g z>!(?519sHx`}>Lglf6H$?>z&-%`2Q9>=;A{+WN@x@#o>oGf0W_WUF)6DQvpy`(+zP zjZ|!~eeeXGqbuUg=`hP&xf+u>CKdGiN_TLTC3dCxZbGW4To3>$!koN0` zBe3O0^GScyGwfFgWU8I2qnS)b#gpX7ADv*!?eI?D6faSfn^ zP%|Hh_<9tM9PmOmIw1hdvG0vz)()TchYuPJSI)pT>I1zNyzSBWfIZQ{g`=8|6#R7m zovhR6hlFRel-Fq@L`BMh|AD;*qWRx_N~^O2bAvr#kY{8xjp_*S8_lcJA{q(W>=Hab z|Isx6kmbZ~ptKAEr>%u_vrH2x#!v7U_S${I&{=C><AVMbi~1V?qqRV#VL~r z;f;%nd+O834iXK_RXFaKaNGSsT#YN+4g(*wha?9#0v0}jpOMjNDLD)cu zsQF--w4k4iUO5>omeTskNn30FG;+g%0K-#ZFk>NrzRvsgcqUqL4wj)GQ@8Wb>rbs8 z0SU7F_E&HJLRW7wDA<{jDpUThefdvzwXNoqlsr_+FMxP?#3H&_$$Yge?DVhIFEGtk zYbRNSx%pi%&!E_Okrq~Ak?c6N`_Xx^==u5j&wRifeWBL-ytTJ{{~M$_)^|lmw}Ezy z>30dUO)o5nodQN0$KU8CFR}d*jLY(hX1z#VxC;^sI4cT+V2RW5s z3(BOhq1M@RbwfuQ#PQa!F?&_VuLD{?LMQrrW~MTAXgtN|i%nnOd1mIalXtsh`%!nU zaGRY~`}6;%bh&8rN&vOrng~4D3PA9nffg%53N#{*Rits*UPYVtSPaFQVN}oi+RyIK zhn7d!xv<>B^b?%#KkHs_*m*R`DQSdZx)*>hO!so}&$<_c38rG#_R*P=)BA%2xr)lL zqb10X*~`7G;yJs!Jbk)R zD$Q;B#Lf+)s*rgvd4 zUY}_b;;M)(#8vH0qtHgs5>zjaXEQ$>Xbt9u4kk7zYtE72<9k+DR;o_DTUiN<*+0O9 z-o=u7ia(6{8=d=Ao}Hb8nF|3qJCV$JhA@8g!jEcX#NuEPxp#M03Ytezs_f}$wf5rh zzadD@^b>Rc(7${;TB75<8fI}Ee?N=$KkyZuL;s*4 zn5^|LB9vtO-Ogd9943-^`45rIzxYjL7R7(8OaK3xp2$+*R&0g9mK;TGh4+qH6j~+u z2|+~~%SEknS8rQL@!lp+ZaH7-EWwhpcpLhRxHFqFP&2n#0wV;%%!IcwYZl}1xf2G% z93Ab^7gI%!-q*<;=873Cp3hyvgoJiq(0%*?!)MC!#IyEUyyF14gr3c%G2J960*Q_L z>9K;+SM*40{50HlkWG>1SPOz_D9ap@@XYV;)qtnjXDAY zjge4RQBt$i%J@-};vtac?pf*l5mgJ=9H+Pc%{0;BJCDxt-_TNv#6chcfJ&kBZJ_~bfC{Mf;DmmQtm}$`z^S* z_6XBgw++~&Oi|9aZ^MPkA2x@hmm4n7e%Qu$SldsJT`iGYbhby0pk_voxL%wGVt>9} zNfZ%vr)n6dwkDzH6n&yqRU+FFuEK~Wk!Y_i@$FVU=Bcf4r=YK@t*n}cJF3{xmIBc; zQM2#D(~ef*7^5GY^TFiSLM>#uofRO{53{MSCJ<4S(Ztz`G!ZVT0D0YSn+}Z#;h7>p zHVqn99Ht-p&HS>w#Nga2Wk06)_G|O5IsGUuX0ti*2}MCsZ+P*#r|{WNz2wkAXi!ov z+43{!JZ#d~;W0Nq40BePi}SYWk>r`>K>2k~a4{o>n!e^k3#D7#?_pO}qDXPNF+XsG8L>j35vO6LH^hurCBet2@hF;RMAi&D{vX(s_8tD>L)_+C?#3(yo4?(~65o0G-&r6-{5E;@9JAKzK=Tws_q zS<9RKyq-byjN?m#vC-+tRP0^VqtZ5pKxEA}GwhTnV|lBH9i=+ZWh4W(JhWy-F_M## zY|Zq@;0-W#H13=hXayoY%zUMlWx&XrQSS>DHoU(29!+_72rP9!w|YLn35rNn6XrX zBJhZWuaf)u>+j(7COlrU*)yailwF-~XB}jzB=OJRvUj9sUFNp8gJDYt-^Y`l4f~=| zb(IjDaVUu=;Gu^6B-wB1{KMowuMsJ{v4sSZax1CW+ z5jekAHzLFN zd}<07JF$WPvO_H@CfG+3TYvQ2}}~etSRRM{2*h8>An$I2~TbVbRcG zp~MoRO|M$URJGy834S3*Gc>~G>oFH;;*AbH-=(W%zHN>BdRGLwsm5 z479OZ;tU+!FPz~gHte_O^8F+&q6*a6DSYS=O=xq(=$&4G)IKfj(PKm??D&V}3_vl& zIgIXqiODtY36j8DzFO7B3B#(^RIDe^Fe4<&=o6P+Q_0Ius@eV``wOec!Hi8RrcpFqZDwjaALt#t8-R%__Zq%Vy>jZzwZVzsF>QL-b) zP3*r-LF2JyC8-T~a2Fj|yY7s(?m<(aY4-i)kGxg5XA(ukv^0e=$2s@H& zaHSWxUp0=&yLPs86g%b4DGM;;j*V%Kr%y%f<@{=YC6f}s=VrWq6|xntEKd&Jc+^H0 zH_?o~+%D54PHTVe2v}69y#hqJLt`-`O}fckLB3&t*r7Vt>O_kw!M{lJ~-Nt0mr4u z)cPDtq#{`zbpD>fn3DmAFlgIKn~|tH@R`eZQAJbnsV_d%MLn^7p34D&$;Idjfe z!3%P1F(abI-n@y7=@ru8DuLb>`$AWsxHuNQBqb2h&8JqQ)bbC7_?Fw;EZ4#|BS-@N z*qJIhxrY&Tyo%h?9!&=;BFf?{kZWEJj`^ZUHfz>r;d=YVzA#Cs+}SKHq_E$EE@Xh4 zOYm0BhkQ70!LNNP?DD7CRDObYAv;VPg+|7g`#&>X|1)R!hfDiEZ9F%AGph-I(+7n& zJoZO(+I-v{fyt<8>)_h06CXFS-SG7s;gvH`A_;B)n^+k=?`|)05V2XkysG@3HuN3r z*&mM_U}hW$#Cg30h+XlVg3ri;=y7w5m7lav2FqaE^NYaT3`DTpD-y6BMNJGXk%1xw zg&hCGhtqzNK=}J%mW}EGtP{Y+`Fb#RN9R4gY6$;YNL}5kZn4I`_n+g*Ddiy;aVHBE z4CZvCj3Plzq`wQom7-k95C!7#T1%({c=u`=uA zTVV&LzWDxq>**Zgcu-M8wtB_(iBhx)A7AHxX#tKZZ*NUYPA))@r|-P5r26ycrdJUW zEgw@g_TyS8avwbH$1aas-|LgLDXYhC73+?S1c#w`*)=K|c^z^!ZN2t)mie1(3zW)OeEm>-q8;7r*xS zS0PPxD%1oQQ1Ugr&n*?qDdymh&IfW5qn910{EH8?tp3FZR@BY6l?9$$Z_ky`Qj}7< z7VD=HZM~I_$D@cL{$ymBz2QnP3(L%5DxqL08>#SN7dtjufW3vVpU=%Agx5@d{h{uy ze?)${a&~(+QLcmG-lY2n>k<*59vUz2`Dr%UTV91%V?%*11dsg<~NP`_2sPo zjwoQc)g(o^%65F%kry%CLmdB2ca`z;x^mgmhmWxAeu=@Wdp6+j&5bCYEvMh#5=bh3 z?ye%2R8?J{EUiwp)^bNjA0Yx8eGc@hKX_@v|5tWFmVVi-E;kg{I;<%8!fWYVYV}6( z3GQ;7;KT1tNb^-6Yj`iT4q_lHQ0KlEaG$QQXiu<&p60>j`xB{YQNVkD}9eH4eV z-tmFXc@C6U>_g(O4gu))fp1<7= zwy5)l}UBq?c%JT+P$P>gZ!GHIB%K z?oA7;S$k?yknk(E=OOLmswM5nM6Qtw_hi3_v_W<_m#CH! zys+GyK;Zuyjp*qVhEGE#Y{p3iZ&`Ke3Cw{VIz?((%x=pru~CxP9}O*o>=KP#+bSwB zNf{5?_vi@)HN=)|fT7SQL$823$f|d%fAuG(H%Z2--)UC{mY0v&K5H{b3UR4d!l;(J z5{Ox7G3PHsk1R6GFZnk^4~Kx~0P6qI&vWC;r2@W*kAFpQ^8_?3p~%l9-JO)_2?<#z zsA;opsMP0-ib;`E8x9E%zqV`6M%g}hCJKy?qqGi>6-!WO7$BQJtf=U-f_A~dS{=f0J{ zbTjH39on!!qmBw@JrZ$tmiVhs{~vd|{PV>te~$tFg|q0tdtqM=H#f^Zkpp`UMR`fr zp}6nqX(iT3$y`(>ItFV8)y`CZ`esoUmOLGZrIyI(@+EPIP`w;ijshA8oB&+3OE>vZWF|HG& zN-8Q||9{GR%dockHr*R5Qmlm*m*S;3!JR^JX=y2?xEFUPfug0jJE6sDxJwA`Qlz-0 zxO;+zy?Wp8%rozPX7%Bn1z5w`}KgMaRf5LUyF> z#)lvfU7!PL4?`UHb+B2|bJnMs<1))N+S^i66*VMrvKrsUyWvCBBZ*tS*t6j03w?9Y zTGyT9N|}+lj%A{*9*a0CFaz)}KMUlTH-ec&_Uu>AjCgQ5`r_u6O14|~NGe^m4aTOv z8-T$K@Jm%t>4!lnBeRAQ+imLdY?e#E1B;goq!fXU3-VZ>-g9giFZ~)cXfn8xq7=|H zzAVU)g@LKye8F7i_l~lp>T2F^)CnVPX+Ne*UCCodWcht@I7a#oQG*$e1O|xSvb^O? z8T^9P_-$0ZxbA?Uo?e(?|I(f<@HQ_&w6^B(vS&y&&4{0k)}}YFVkVC7-u}tkm&H&# z6Y<_*nufdWIOv-n=C6mKK_E-T+q`COZ#j5uxxns>?PG`IS){dLY>P7-#?{-c{r2KZ zWfiKSH1}Ryvwlaa^j-k^)b+ZjQm3WG{K-J^#tISjt6jVN8Tt=#9o8<3gIL*2A^nZ} zGB*S1xPgH97^!6V8$X=cp=@`RFlvP@9Gln}UHK{YK}YLa`3k9$kGHbqN|nyzUN5sc zYO%eeaw!XQ3V%}X(J^Y%{`%_0jk^7V&WE)}^Xs^R!&OP^l`A_0_X#o{6iFQUD5Mw3 zw=yE0=Tj1eyebZL=w)PLOQiaF>YM#;t~e)K0j$ z6i1gM(V(*p?f8K0HziMMlWckByMM+rB-Y_#d*QP_GH8bS#EZU+lQ8ycx=&7yZP8jn zzIlpsh2wEy>%05)ba{{9ByGx`0B1;MKs>+B+1b(RWg;YpC|iCm$?`P!2`Se|4*#-m z7N(8X&^jYy%`udZZhNn;p=Sgfr}%Ty)c6O(VZcFm z9FT8vm8O4rjB{n-^3*92ygc#B<-E+PV(QnOKx4?8&)w|dGRvE7_r#H1&g zj!Ol+O^XMLhE7FEx&u+ZzYij;tf?bSGjSVcw2HLUHYUDi1eE88Xp?$#?D18k?|f*j z1l@I!z#V=rITvhM%n`VgL^xicmHp%PL!@o+z~)sWeTd4mhD5GqoW*WY+cD^FQY-){ zQlk6m7z6o6;*lh4Qo5$;IwNkQB_oOS_WYUJ`XVCZ(XCKdfK(} z5>INQTGa??LK(**I;ZLsn-m?&&%>kDa}f7)k-ByKAu|sT!?BjK)OGhp5x>o3>vzTW z`$?vH=^bY>YV3t5Umh|6+jpBx3+1jCU0qOLOclG2#92dmpT0)y{&=58oLLmP#K8lBcS_h{!7c0>St|q)@$o8p*HB`ps_4LgB)f%`k#$j2SMxKYAL?C#ZUnEOj z{!`RsDhAKc@iT>!b-D%pwTa(CcJ=|g=XoLYH^y0#k`ZBRca%ME)HsFdSV%S-2;KUI zymZ`K2R_{_6j)7Di!!COA8|%!r9YK_p*FS_jK(J?)UkCu200nt6=`ZaK8f-{!0lZ{ zd#&tviH6_mw05d?Kw+`n;u{o|7d6&zX{;h1S!i2LHak?hMcfi71EqW>k^3m~g`(=S zt`8LRsD!NN{7>z|>Ael&8#Y$WXuH?&bCHkqeUI+#IuMs%g=hu8$G0lB5%Bf9j30<& z16>M3!!`NxpR4Js_=-m(^B1i<3ohhHhlHgcq!j4rZ58fTYq)iTy?8_fvN7H68ge(j zo6p;eV<)yQOiqb;l1H4?_M7hk;(#P6(X6{@ z^i60e&{dm^`!~7mKi^Iy+^L=;CMI4!BgwDma-7ExM4ucnbWMNz7JRiIlKWm3$d^^N zjNUn|b#%E{a~XZ+vXxX9NU3{1OZ=P_O4W#BbEq=TUv|7g+1@iDz zu54}7G?YZ^ZInx=z467bP zZ~A~WE|>j=aC2iz*=wfc)b>vbfK)NI(dcR9gQJTzqXGtC7OCi&DAGDl5dsB`vSsDhuvg`rMlIB_m z>VT`c=g%KW!hzJ$*$xxer2`9LhW{H)0|90$O4t$*cjMT=aW}fJ{GB|(Sd5c z(kECLH8I`VdcSQ)Q1rbleCZljO>MbpDP<#KZXIZ|@P>!jm%fi+Ol<|Dea~+WR(49h zC%NDNIUIoN0yMezh%Vxr+&0cTJEzq~4oF`V{BU28cmsQjb+T8tveL9hK~V0wvy7ZW zr07{HC;liXw~P!mFwXtqzNWUtLU9hXHOLzp5X#SrP{wPF{pOb>WgEj`guFomyx3FtfmzObZ^d@F8)WS%nlFlQmeX2%f~aDB2-gx~t20~(C#T{b&z3)NR{xD6l6zkbOQx8$#l zb%>lZJGS(7niZ=5{c#mMl+CG32Nawya9Y=g$XX*o(^*c5Iq^$;Rmd9qwGWnkWM5Cp z^jW}dt>|zTY5M&}LY>d>06;oWRtym!0_^#Qz<$nho4DP_WM@k)YF`Bez8!>Euc-l) zm&HJn_T!c-({c6;)!5a*G5xCFsFyDbLr3SOu0@1}E8SIu95d$|7vkw}5HlAi%9IQ* zi;-Z%H~5J1d1t%Cxi?}VbjXc0V)$*hQUmzt+2(FCe0uc#2nW+!W0EvOW+o#YW~*eD z7jJ1uZQ>m#W;2@|#khrc9p@|^6ysudZ(5AKs^|1Lg$qXS2=#v2_1)-Onz2!Ltgi92 z%FmI#Bs_mn74GEKcN8-Ykr+M9OZUdJZ^bpkKZ{fymOOQoG8x9nJw=8qvHj zz_9uCbrPAyH(yXo)X|C#M3Fnrwh4({rVh0&lChte8(=^KP(;~VHjY&azSx6zBF?FP zw`CAE$$|(*hGuvLa=%G17l85TUw2XNJY-U%S70IC1Wfp7@YSl{@1d+U))%Ug*Y{!H zR%)g!i*${6ghwp*8}N|(DGE0c42c+Tn|;n5U;hZQ$xV>3Q^o^gV|q^veGoq*<}1c} zA{f=+`2^cGt50G{W#au#pS!uS{`~%J^_|7O;a7qs-{Ne=lCm^608gamAE#{G zv`)k68L(rZgSXTG@(r|boaCNSfkUKBb*o2SClx>Q$OSGHA>wjpp zbBgR3RN=Oq&iE3lQ7V8?u|W@&)RdbYbIhu2<> zP96AAzJX|eziCadGyT_htBooMEIS!?VeJd)y1TU!E4fN@YI#GwcY@B2W#{`r|8_{SHjnM8)TP!F$KLyI{RRxC$HH}kUFLOp}p z-Zd-dW~R33U&4n!fn7I$%9J5*1DpYBWKeCVq?kY<5ywRO@yoL#F6~Q@rheJNBFZwS zhsEksLQ6#3C9V42UgfyzYf3$}7}&9!*yp5BeSL*nQu`|6GUJTQCxOai-3Tfp$5WM% zhQep1t-bw;l^x;4IuvipO%JVzSkO$FiqINq<=Le-G<_+Yz!?X+43Z(Ergp2JhyUOo z6X`K{gwf>Hw<{y6r&VO+nZf|%al1P=)&I~vVq@P$cC?QJbPtIJ+rL4m|5vwN@W0bB zn?!$L9e>g>Hh-mKB>qmv{0&|Gx7&eTpv&R22ksvf$l4}M?}e)&-e;hl;9E1@Uk^?e zV*YvR|70u7j!BOU<7eSO)}@AH-FUQyj7La~ZW;F#AmLTFlVnxc8~BJ)r}q{QOykJT82>lNPOEnB)Et zL#EC)t7j)@v$UUNEDBUE=~$1d9zSyB3dgif66~=K?NM>ErMLrf!}p5lrGv(rQJq#_ z5%?+e_`g&8nly1A&tpHBIXDONdPrurP6V+ys&>>>C{~qi0cy8kWt;0NsEXp}=QI$x zAD$@d3zXzLAJpkMSH|%DL#X6tJBjX6YO}&v?H!c7qaP0UTg=>gQ;0+mukb+` z4iJwEwcM2Heu_lIWYqDt=B~Gv)2hy;-zV=#y{?hjYhTX;6V;U=O1Y>zFq%MK&joNn zHsn+b&y#iRRx#<1B5WZgG^kP${zn$pymz9a#Lpvgrge5Tn%0Ogf^#65qQ6GLy&>=t zysQB&W^lb!5C{M%%u{oR1$k;knmS%0B#lQ zSRF>7UVKU1KFca8=wi?nrEMM?1-a^9MhZNnJSLo#ZzvMJ+E*m(g5wTD*^ z{WU`y6+GxRKX0GCHO$bmHWs^s9a8&*6k~tZsr@~ExY={m3&J6PiQY&j4wgo(!ep9Q z;7r>t(6}ZBnu)x}VJ2Aoo=+trDJ8|+U-laDlZ-5SHGW^%HG${kaXcy{CT|~ApSm6; z4GwLVpRKEl7hp>YC$5>!7U3_fM)Yf%4U2BPIB#%;-4w4D2icPCk|fH=?HJ6!k(V4e{7PAC$G|F)rjq-@^}$R|4PsXT*hFc`=Z(SDccJ3+k=p=h0T05~O~Y0nrN|WMs{Z-)BD%x*F=oG-)!K9qhB%x|In@ z40|?em7-&Jt_037Dqn~*qq5W(!AzXc%Lfhp<|;~%2H55xs;wMPY)e4<8TL5Yc&HPg z`jxw}#`ovjy~POc`^RC0UQ(Us(V-M2lCvLhm{*gn$9;Ph)(eOvo-BC0YP3?4>OID| zG7(c8$R)8h66TR`$$(MjX43j+VO!4`5gQW`b&vG3NSpN(oY@pks6;=>lU#CUou0%_ zuST!%D_WvA2feyzpV_38989h(j)zl=Y<=5}R}aZg%kQ!qGwrsSu5=kY!o^BF@cK zPQQ@^RsSYBPSf0XB+uf!qHR8>ZHA4k<-Sl^7s~LQ%dTD2FJuv@$IbX9QI-8*kZ-m1 zHsB*qS9fP&S4ixrO8OmbLm^BQ6X`xSgCVt+U=+y3T4GCL{@F>a2s>+bP;>^wl^}(W z;q!5qf(E?4pzb9$wq6moO(qN4u$@R^llcB=I6wd4gsRMAyauYcT*; zH<8C~R8et$jHLMl8XeZlYREfU3YN`7Wyrhhb@^@rsytenl*VjkS zij!#){!?Wk#O6Oz78=n>jvR#(0-)oA5GSW_Y`kUKrJzbdU#GEx7655ToXV4=?=|Q- z)uz{iwUJ9WSJwdmQ|jiiw58cV$JC(ZqAb%b9b4IKkx%h-Zt!#FiAex13Ut^zj*S-% zkp{NP;j8{jF93dFq08pI=A-b_2B(~6uK^y@x{f66{v{4*>j;Fxq_q5=o!xSdcL7|T zFU-I>#R>zUj+)%k)H;PVOVy+Orpx6QQC*a%1@<^8tT<_QGgKE}d+?odFP_~w4#{wO z$<-RGf#tcS@L;T&3)msn4--;-{jm{k2(x70*x2d~+ea8w7JYwgY&nu+8Q|hc-rO8i zI{1nQ++JZT1PoBjyuPU;m*an2m^8Aj!0D-|Boi_`k~RmBT4Q5Z9qsH3Wpch$>X5|A z+kO7^i%dh$gcyE7oSdxUSk>rv?wAXvfS@G*#5vB*2og7-5lgo+EsH=wrX1vd5T;*c z`s;~rz)H;FS=qo1(#tlhejYep#Ke_KZaq!@tK1uxb*f<>2-F|Nnb!l%~5C|Ntr*hG1hy2uB zHb^Y6WuDhi?DU88)|ihhA*CAruZi9j)M63_fL=+byW#1!9bf*)m535{FQRkZo##}( zkC7#+j=?c3a`Nza{wc zX%>KY3^R>Z+q&&|0mR&SsBEC+%Et_enuC@0r}z%k)a-{k;yM&6>V*$-pJR!u-{2IOGtUR#PC;zwZu9&7~oOE9Q2cv@Qjrgn{3w9|e#vK?_Q z_*yVC2dc9TiW$Pw6$GtK{)dWzDv!`V6ob6e|5!0NDFqY*O3HsvF%Y4cQ{@!4`-e;* zG4n?zF#K1UpnzB3i07>Tza$e>HNM$e=^!m!8pZiJV>7U4_kia=(PzS2lZ0G%UOS^<5}S;m9HxXONkQa!#RCdwDuMiImtTtB7X@3 zhOY5Gjt=@6p)VaK>#T_bHYAUdz?8<1KRNEy$-ZeZw2o8MmI5F6AJH7U%mNKFP|N1M zTA!;hLWI3Zly(TPw>y0R_~x>|-xAs6NGpar z^J62MMX~~De`-7rA?>$v0a(OoOx?g=l!Tt`tQG7Ii4L3q$XrJ6d8Vm6jveR6eTzwE zL}=qA$KnL!r(_!84#R4dqGt~uvKWl5B*YI7r`kJQMJeW$Xlm_tsXY?-H&Q^#wZ#00 z9Xa{kJ|YQ8e@nf@^8mLjhwJT-!Z6dzuz9Rg=1EVfs-)iRGMfKsNi?@ z3X*Bw`ij$ysaar1^ zMv6+&u`oYnBLTd6W##HlGsMLQ z(1dDrZgiOFX)iCJHUm)Lk1a0ZrR`WWPzVM8W7yeuvC1k;e4)CZ*ZoW;o%bipkzE`I6Kb|FT1d_*s%co}W0aWg5(zl)S9jH({vq~fJy(xyzvWpMcVbv3sp zsGHKbtIECXkJ%E#o(H=^-uLov<;SlT8b`mz<|O9`zR#yFCMejO?JlwS4Lfw+i6{cw zzj2}<65aZGv_Gm`oX&@I^4>eAuJSp4qC@&`pX{~Skf%apYV4Tz_L0B8m#P~EJR%RM zi7C*ciMvm)Ue6)d$=I5ZbCGet<=BsttxwIe0BBgA6VYOMS?FVrgPYhGXH@QLlz?XV zkr+ia_FM)U!&Cb@C7ED_6Sg*hah_!$e!Vn;G%5|01v7-$B1MsR9Op6eZ+B@l!pBVq<2{r%Xw&je@C(%(S(EODrpu`Ry&3 zC@5p;O$;I)oDAY1&vKujtO53RIugD5>jQaT1~d-ghQ&VIY^YPL#r7LDez8vxpo{kb zr{D*Msk$mtp^y1N<@+LvM$}%9f#<#PKB^Zyt%I8%JKH}xUn z)O1IfpG;4a^RxW%6L~()Y-i1C02ShC7ylP7Ivn-K4wTA6#(xb+XgbaU|z4LK|T| z-|g`fDVl++Vx;a5qs0%{O&Dol?TH>re zla}sjl9h=Gv8_R79h#_0_ z%e^XlCE{5GT=-Yl=XP12<4%*J)HrvjlW07qI3)3FYmy|a1j)9FXqdz~93J-643KWz%1ay2W|4N_8bF{TJ z&7|Vxux5SQIi>KWio9d1Q}<1fS(N~K2`<;O&f~E+ql{X-fmJP<7As3!N4scQYHR1C zPNIs$O>r0CxY9?J4?`Ql^(dgV-PFiIK!+nXe>KqPGdXa7VWZb4mXL_j;>!BSB1voV z3v(NN{&Ccrpc=%w^b;sk2 z_#;M@>DO!OxJE_xD49&-fRxiMs|QBMWz#w@aUB6-DM!pEo^CgHo<(qp<0)M!+w_>) z{_khNvk?`^IzVzY|ArM|<4Uo*!rjZb;{@kfStCcP*C&4k_tG%E|#0*K0%tGr?xkAc8*CPT=@&w*QzlV~Bgxd$f9G3z?c|G(~i zljZ$Ys#1j(v423vTI%$eH*tC<()h|LGan9^{7IOfuN;!quggYy`51?3SvnkF69O4M+(C3=7)$54fT8A| z#D3=JKId5OkTFrP!dixy6#1KzNN?4{K;5I&%%@0**L zR0B)AuwwJ@9=OS6_)?l`+IVSqKIEKNpos1%D`kLW*+9dVZb(WSa2X(PuuvBp2oyZ7 ztgck6{r>Wzzu6@Bn;JESB_Ec6n;kbe%xIRK7qyyHIqbP(dS*v3Sho}#caUmOKveET zKM#>?KK{6?l9=)!l{?{ZXdub6)Q}V0iFLc^&-TII9#JftV#N!4)qS=ca(oWV*jP(L z%-ScRrl38arE>$$6rdqfXo8pjY>ajoV3U^ycYPZ&S$4AqY>r1tLFcDi2t7b zon~(dF8PmY)%TMBRI7fLbsM<8aBFfw`~>yLE{a%?yoV(2W zz_1mVCuJxKUQ;wlwC|rgOx;mZB)F_-6HG*GXaS3Qa2KxZ#8 z!lCB@pTB)Ozj1kE-FfFuhx5zV>}L~-XJ>9%S(4{I7EI%THvD820<5L*!e?q2Ar&*1 z`(htIt|G%}3m97LQnC#Vo5fo4XPWi5Z{U|HyYUPVpF?!A&h0BDhM%6EWK>tkzO&9O z(E-)^M6wGOKiwYtGF3oyd25&32jG@=7q}uWfmth<)jmgub3fi-0s~zJQsL+4%j#(FSVtxu*TZ?_x6Na@(T{eSHaXW#1C>V;bBG*#kCm+{kKLjI{(?E{dvA zKC3nMr3pGcbuDb3hdGi-sN&?yV!^-t>!JTm|JtofUgMUGU^H{P(&})9abP8e7B7}( z#*KBb1q52Vn+WvXpQWVq?Rw9NxDZw9kBBRKnNJyWc^&~xBm?97Ee5;_(fY8rUNSpV z1FADw&Z!%EKjLNf>lI4nwI`RJC&D;7sIFloXK0JR&l`ckXHk}yZ(aygvPm-#Z0$NU zbl1XFaAs#$JB@$fkX2bl85$$IX^&-n^rl*8&s=6ZL!SzU3=cERq_E0B=hr*>RHf+G6`G44_{uhrsN3nGw7!Jb(k>Nhtv`y+k{QTQs{!OKng?VjM zla4UGCg_M=NC;=)VeAk)0>Qz~cNQX0f)K=7fp)sqUN;AS2U8IQ{rzu$xfkd)wz?(6 z0XrT)Li%&Fi|JJMLcN%_?i5EO?#02fZEePMtJ1=~p&jck_X%)&pfd~qGX9k zpr(!4WDMeRXJ&$Dx#VS)!=s9fM)=oE&?dW_`oLUPi^nylWG`iJ8BePxgKdBJ_Oh|n zZTgU^11kV(UAe5H>%z5E0SHnU!{ZfPv*(kh>C?F4R5>G-#mO7fOpSCDyu3*e^U#0W zcIJjTJ>H-;H2U+)EvMO#rb}!rqanqo5C`OgV{2Xx|6?qSMq1QQ-*;S?+Mt9y#w?bw z>zAoXMuN~<*lp|Ft4J<;sb?p8WCY0P@HIZgc6menuxQy6!Rx?*3M$Bu_Jb58_J8P4%1gZi#@`L_V*>^S6_rW&?2=mQ!VMJq+q7yACTK zQ6J`X?~?{&N^5!wIYH=yvR|&pOI~R_U38kQgDayDc33%kR6$&#&_u2l#i0}JGwO&8 z{#aGYTiW65`O)hnuEN+5p!gTux)|RQI~Bo5kQ4nX{&Mq2Ds=R?_En;;l*zv60mt;h zUG{?Y2P=+?+9w<%X2{`wVrB#ef+4Qjc~*NIx#6$S-P%fY2mh=d6aUQP{;pj|D~a6P zV$8WDZENUf-!zPSn+~jGckdCL7mQrqoK{b$M6ibn60Sm6HCh)wVk!nhFeFfca(VfM zC~!EowCYk|9*jUkVKGAW0D$80cgR0ACXF!heVW^f(eY@0r2}?g5xnokZd863Bha4Q znqRJWE{vLBy#B&7nBH+(XS_F8fEGmdX}@OUuukY%owCD?W4bl#W%DRUp2bRJ=qYQl z4oBiBUbQN8#{B2hqNGuk_?YTY*2kd?EwSaoCR=Z3Id9RuHX+Ti<6<-IzRx$~a20FJ zXD6lYDdMru-?|i;mc19^}!~1{D07WxyZNkcy*Y#4WI$lzMN_AAERg0 z2PJ^>O5i_Ua66LO(@nm=Ra9kX&>XiYM)$6tP4p?N#tz3r5605YgjCg|V=e(qo7W5_ zb0W&)G-~KQ3B!gk5Bgc<8(HI4Jrjj512HThT8%328+u7u)}Kt0RRrpM-KmLBz0Zot z+Sx5NWn-@I++wlg%m)vI{urJ_b)BL$A|E zx6J$yMD6m>K`MdrVr$Yjnqr$#wW2Ns!~sT=)H(8aH#ouX7pdQgEq5ejQZqVv6wS8; z5*FDedT)z+&;xLE4#P;Y9X`6#@rFPy;9>vm8IRXssm^QhzV$6p9#XG{HlZzB+X8gS z8`YjoaG-T{H@!iiw!OtcwCuzRP-*i!h&){|8WlMWKUtm&Yl#pC?qL1*2lI$`hdl&) zM!L!+D>Z6!jX%EMTiY4*6LJcOys@z5>)CO?bO0toXFFr5B@QDoU3hBUorkm`DN$<1-_xro?~~3Ikyplzh!xX`QWQy``Z;_>o^hp2$DI#5R-NmYf}Qk*F&MTI&UD zfu?i>|L~9OMf6-PEP`74i1M_4rRp#g;(=B2rxVKhXGca(;X{p{b(J=vJFOT^Tv-$I zeXz0(EhCkIeZ5GWfQgn973Vxl&0J-~Zd7khZG37@85x1kSMZgrfTq6vWOBoUT-VW{ zv$Y}a;_rSO7`d4_4+f1HO1AsoIL1DxRI}!2j{6$Fn<`#B z*acklh=}Y=z%&}=#YN;uU)Qym{MBQ7JZ{J7y;Z(9iYwi~9$@>BGBQ_rNW?~S1K+D^ z=Q*`2Wdr}Ttjj`Sv8u{&Du4u|D0oTjQ8{Bn7Cxjk+1Vf%6(cuUaA&ldTt8_&LDnK8 zK#lJ;mq=Urz$gF7Ro~T|N}`VakHh#@B6R+++aHq|3F>N#iW=jcV3(%o@%7C;(Sz#a za}KP0kOqqxsM9*9h@fF~1{6g7sSYBOy*CcGRa+Ee=e`o9%MgcU@vWZppRJO!vvJRP zuV?8?z`UjQzrmOY$gN{X_W1uyUj zfseW6P4IB}t@@@ani8S&cku$!6CQ~)O;{zhFp!wLvj*NM0?P~hEAaHL)>ls2{H(1^ z1V4h|>(Jcz*cG5EZ+GhQ>V1lt!P8B&T|m*~&N{*h-#2G$5Yr4}cG6KM*M< z%yK_W^6#*k-UGzu9!m{C*AoARNq;qw4V+xwcH-iNw}R1OfUe?NjV<9kOLknu*R`*~ zcxKiNxE()Y?d0ZOo*n|t0!l#2^RKyL|Kvveuloc3#rt1;4-a>1|0nOiFEG-O)5 z{!94(ivW;rs^CB_)YGMD83O5YAv6G#FO^&T3AWUe**V0F%@yuxrZA9!1zw+$0?hyH zDTM>>7(f<$9{9`PEBRG6v@#R`tM_J&4s88Uz0?CLY3( zv#U2}8)yBsA8+<`l_PeS?d%b;lB}CtPnIis5^p8D(Zm3zp!U~8y;2fS(p3 zYJ3rQ2rAwz59boc#2n5o_5TpE-ZvzGO?&H%K>6wjDM`TRa1YyNXc!?|ubtiMk^g}z z#v(*gBNmOnUu{cf-Z%JnPN2PEqu>3Htsi)Mb>uqlk+d6n4N=ifLj}$`lJx={$HH^~ zwBUIAkFbC&_3OXn5`by>CoND`WeJ32M!}4y|Bg;Vc`}mu5Dv|}3*agUtm+-@>?}9p z-^~g&cs4Of8tjiiWg8iZ(*Lsc+kGPQzp6TM^aOs&K3cI-k~EBoal85*^>c;fbXl3}oJ1|H*HosEL-UR7T31e2oB^ z7rMIF5HW7h@8DoW9WcAb&+hZ3U&GK4eL!=?^8+}DDsq~NY-(wYnJubq+5zlM2W0B~ zJBW2wjB4)kwov_hIRFc$+C8#c@Z4 zzp0q0)BHm*d7}wr08DpYpD$y7RVHQQP{x9NQ;QW@A5O_U%<=A5ow)Q9?>k@58e3dR zm?~{hQlnqGOr2=+-q%=Ms<=0IbhGDt%v6{oi9lb)4K=1n^hGy!j?Qq}oRRZcad9QN zd>p!*{PF%EfCEx_&`!u!?RbO48%P`2P1x-FegNh(RsaM6phbq&sflM1f_U0+%2N~= ziDTOTpaJW5k8ElWttbhdLFJlCy1cb@|3g;P65kH+XJ-y-9ha#rt8(3AcRNdAueX1b z%HYdhWn_mo?Kk=~{pSe)Q}7QXE{j;B5SZWDvR?4xLZu;oV9@E7#j~#7)KeeUf~qPe z9>?XKgb#F=MMd4}|3AxLxZF!RTSVYf{9s;Hm9iI`>h9&T z^8YyjaBAfFt5*#eks? details, }); -enum _FlexAdjustFastPathRejectReason { - wouldGrow, - wouldShrink, -} - -String _flexAdjustFastPathRejectReasonLabel( - _FlexAdjustFastPathRejectReason reason) { - switch (reason) { - case _FlexAdjustFastPathRejectReason.wouldGrow: - return 'wouldGrow'; - case _FlexAdjustFastPathRejectReason.wouldShrink: - return 'wouldShrink'; - } -} - -enum _FlexAdjustFastPathRelayoutReason { - effectiveChildNeedsRelayout, - postMeasureLayout, - preservedMainMismatch, - autoMainWithNonTightConstraint, - columnAutoCrossOverflow, -} - -String _flexAdjustFastPathRelayoutReasonLabel( - _FlexAdjustFastPathRelayoutReason reason) { - switch (reason) { - case _FlexAdjustFastPathRelayoutReason.effectiveChildNeedsRelayout: - return 'effectiveChildNeedsRelayout'; - case _FlexAdjustFastPathRelayoutReason.postMeasureLayout: - return 'postMeasureLayout'; - case _FlexAdjustFastPathRelayoutReason.preservedMainMismatch: - return 'preservedMainMismatch'; - case _FlexAdjustFastPathRelayoutReason.autoMainWithNonTightConstraint: - return 'autoMainWithNonTightConstraint'; - case _FlexAdjustFastPathRelayoutReason.columnAutoCrossOverflow: - return 'columnAutoCrossOverflow'; - } -} - -class _FlexAdjustFastPathProfiler { - static int _attempts = 0; - static int _hits = 0; - static int _detailLogs = 0; - static int _relayoutRows = 0; - static int _relayoutChildren = 0; - static final Map<_FlexAdjustFastPathRejectReason, int> _rejectCounts = - <_FlexAdjustFastPathRejectReason, int>{}; - static final Map<_FlexAdjustFastPathRelayoutReason, int> _relayoutCounts = - <_FlexAdjustFastPathRelayoutReason, int>{}; - - static bool get enabled => DebugFlags.enableFlexAdjustFastPathProfiling; - - static int get _summaryEvery { - final int configured = DebugFlags.flexAdjustFastPathProfilingSummaryEvery; - return configured > 0 ? configured : 50; - } - - static int get _maxDetailLogs { - final int configured = DebugFlags.flexAdjustFastPathProfilingMaxDetailLogs; - return configured >= 0 ? configured : 0; - } - - static void recordReject( - String path, - _FlexAdjustFastPathRejectReason reason, { - Map? details, - }) { - if (!enabled) return; - _attempts++; - _rejectCounts.update(reason, (int value) => value + 1, ifAbsent: () => 1); - if (_detailLogs < _maxDetailLogs) { - final StringBuffer message = StringBuffer() - ..write('[FlexAdjustFastPath][reject] path=') - ..write(path) - ..write(' reason=') - ..write(_flexAdjustFastPathRejectReasonLabel(reason)); - if (details != null && details.isNotEmpty) { - message - ..write(' details=') - ..write(_formatDetails(details)); - } - renderingLogger.info(message.toString()); - _detailLogs++; - } - _maybeLogSummary(); - } - - static void recordRelayout( - String path, - String childLabel, - _FlexAdjustFastPathRelayoutReason reason, { - BoxConstraints? childConstraints, - Map? details, - }) { - if (!enabled) return; - _relayoutChildren++; - _relayoutCounts.update(reason, (int value) => value + 1, ifAbsent: () => 1); - if (_detailLogs < _maxDetailLogs) { - final StringBuffer message = StringBuffer() - ..write('[FlexAdjustFastPath][relayout] path=') - ..write(path) - ..write(' child=') - ..write(childLabel) - ..write(' reason=') - ..write(_flexAdjustFastPathRelayoutReasonLabel(reason)); - if (childConstraints != null) { - message - ..write(' constraints=') - ..write(childConstraints); - } - if (details != null && details.isNotEmpty) { - message - ..write(' details=') - ..write(_formatDetails(details)); - } - renderingLogger.info(message.toString()); - _detailLogs++; - } - } - - static void recordHit( - String path, { - required int relayoutRowCount, - required int relayoutChildCount, - }) { - if (!enabled) return; - _attempts++; - _hits++; - _relayoutRows += relayoutRowCount; - _relayoutChildren += relayoutChildCount; - _maybeLogSummary(); - } - - static void _maybeLogSummary() { - if (!enabled) return; - if (_attempts == 0 || _attempts % _summaryEvery != 0) return; - - final int rejects = _attempts - _hits; - final double hitRate = _attempts == 0 ? 0.0 : (_hits / _attempts) * 100.0; - final String rejectSummary = _formatCounts( - _rejectCounts, - _flexAdjustFastPathRejectReasonLabel, - ); - final String relayoutSummary = _formatCounts( - _relayoutCounts, - _flexAdjustFastPathRelayoutReasonLabel, - ); - - renderingLogger.info( - '[FlexAdjustFastPath][summary] attempts=$_attempts hits=$_hits ' - 'hitRate=${hitRate.toStringAsFixed(1)}% rejects=$rejects ' - 'relayoutRows=$_relayoutRows relayoutChildren=$_relayoutChildren ' - 'rejectReasons=$rejectSummary relayoutReasons=$relayoutSummary', - ); - } - - static String _formatCounts( - Map counts, - String Function(T value) labelFor, - ) { - if (counts.isEmpty) { - return 'none'; - } - final List> entries = counts.entries.toList() - ..sort((MapEntry a, MapEntry b) => - b.value.compareTo(a.value)); - return entries - .map( - (MapEntry entry) => '${labelFor(entry.key)}=${entry.value}') - .join(', '); - } - - static String _formatDetails(Map details) { - return details.entries - .map((MapEntry entry) => '${entry.key}=${entry.value}') - .join(', '); - } -} - class _FlexFastPathProfiler { static int _attempts = 0; static int _hits = 0; @@ -429,8 +250,7 @@ class _FlexAnonymousMetricsProfiler { } static int get _maxDetailLogs { - final int configured = - DebugFlags.flexAnonymousMetricsProfilingMaxDetailLogs; + final int configured = DebugFlags.flexAnonymousMetricsProfilingMaxDetailLogs; return configured >= 0 ? configured : 0; } @@ -659,15 +479,14 @@ class _FlexIntrinsicMeasurementLookupResult { // Position and size info of each run (flex line) in flex layout. // https://www.w3.org/TR/css-flexbox-1/#flex-lines class _RunMetrics { - _RunMetrics( - this.mainAxisExtent, - this.crossAxisExtent, - double totalFlexGrow, - double totalFlexShrink, - this.baselineExtent, - this.runChildren, - double remainingFreeSpace, - ) : _totalFlexGrow = totalFlexGrow, + _RunMetrics(this.mainAxisExtent, + this.crossAxisExtent, + double totalFlexGrow, + double totalFlexShrink, + this.baselineExtent, + this.runChildren, + double remainingFreeSpace,) + : _totalFlexGrow = totalFlexGrow, _totalFlexShrink = totalFlexShrink, _remainingFreeSpace = remainingFreeSpace; @@ -716,30 +535,30 @@ class _RunMetrics { // Infos about flex item in the run. class _RunChild { - _RunChild( - RenderBox child, - double originalMainSize, - double flexedMainSize, - bool frozen, { - required this.effectiveChild, - required this.alignSelf, - required this.flexGrow, - required this.flexShrink, - required this.usedFlexBasis, - required this.mainAxisMargin, - required this.mainAxisStartMargin, - required this.mainAxisEndMargin, - required this.crossAxisStartMargin, - required this.crossAxisEndMargin, - required this.hasAutoMainAxisMargin, - required this.hasAutoCrossAxisMargin, - required this.marginLeftAuto, - required this.marginRightAuto, - required this.marginTopAuto, - required this.marginBottomAuto, - required this.isReplaced, - required this.aspectRatio, - }) : _child = child, + _RunChild(RenderBox child, + double originalMainSize, + double flexedMainSize, + bool frozen, { + required this.effectiveChild, + required this.alignSelf, + required this.flexGrow, + required this.flexShrink, + required this.usedFlexBasis, + required this.mainAxisMargin, + required this.mainAxisStartMargin, + required this.mainAxisEndMargin, + required this.crossAxisStartMargin, + required this.crossAxisEndMargin, + required this.hasAutoMainAxisMargin, + required this.hasAutoCrossAxisMargin, + required this.marginLeftAuto, + required this.marginRightAuto, + required this.marginTopAuto, + required this.marginBottomAuto, + required this.isReplaced, + required this.aspectRatio, + }) + : _child = child, _originalMainSize = originalMainSize, _flexedMainSize = flexedMainSize, _unclampedMainSize = originalMainSize, @@ -878,8 +697,7 @@ class _FlexContainerInvariants { factory _FlexContainerInvariants.compute(RenderFlexLayout layout) { final bool isHorizontalFlexDirection = layout._isHorizontalFlexDirection; - final bool isMainAxisStartAtPhysicalStart = - layout._isMainAxisStartAtPhysicalStart(); + final bool isMainAxisStartAtPhysicalStart = layout._isMainAxisStartAtPhysicalStart(); final bool isMainAxisReversed = layout._isMainAxisReversed(); // Determine cross axis orientation and where cross-start maps physically. @@ -888,8 +706,7 @@ class _FlexContainerInvariants { final FlexDirection flexDirection = layout.renderStyle.flexDirection; final bool isCrossAxisHorizontal; final bool isCrossAxisStartAtPhysicalStart; - if (flexDirection == FlexDirection.row || - flexDirection == FlexDirection.rowReverse) { + if (flexDirection == FlexDirection.row || flexDirection == FlexDirection.rowReverse) { // Cross is block axis. isCrossAxisHorizontal = !inlineIsHorizontal; if (isCrossAxisHorizontal) { @@ -904,8 +721,7 @@ class _FlexContainerInvariants { isCrossAxisHorizontal = inlineIsHorizontal; if (isCrossAxisHorizontal) { // Inline-start follows text direction in horizontal-tb. - isCrossAxisStartAtPhysicalStart = - (layout.renderStyle.direction != TextDirection.rtl); + isCrossAxisStartAtPhysicalStart = (layout.renderStyle.direction != TextDirection.rtl); } else { // Inline-start is physical top in vertical writing modes. isCrossAxisStartAtPhysicalStart = true; @@ -1037,18 +853,15 @@ class RenderFlexLayout extends RenderLayoutBox { } final bool mainAxisIsHorizontal = _isHorizontalFlexDirection; - final double gap = - _intrinsicMainAxisGap(mainAxisIsHorizontal: mainAxisIsHorizontal); + final double gap = _intrinsicMainAxisGap(mainAxisIsHorizontal: mainAxisIsHorizontal); double contentWidth = 0.0; int count = 0; RenderBox? child = firstChild; while (child != null) { - final RenderLayoutParentData childParentData = - child.parentData as RenderLayoutParentData; + final RenderLayoutParentData childParentData = child.parentData as RenderLayoutParentData; if (child is! RenderPositionPlaceholder) { - final double w = - child.getMinIntrinsicWidth(height) + _childMarginHorizontal(child); + final double w = child.getMinIntrinsicWidth(height) + _childMarginHorizontal(child); if (mainAxisIsHorizontal) { if (w.isFinite) contentWidth += w; } else { @@ -1072,18 +885,15 @@ class RenderFlexLayout extends RenderLayoutBox { } final bool mainAxisIsHorizontal = _isHorizontalFlexDirection; - final double gap = - _intrinsicMainAxisGap(mainAxisIsHorizontal: mainAxisIsHorizontal); + final double gap = _intrinsicMainAxisGap(mainAxisIsHorizontal: mainAxisIsHorizontal); double contentWidth = 0.0; int count = 0; RenderBox? child = firstChild; while (child != null) { - final RenderLayoutParentData childParentData = - child.parentData as RenderLayoutParentData; + final RenderLayoutParentData childParentData = child.parentData as RenderLayoutParentData; if (child is! RenderPositionPlaceholder) { - final double w = - child.getMaxIntrinsicWidth(height) + _childMarginHorizontal(child); + final double w = child.getMaxIntrinsicWidth(height) + _childMarginHorizontal(child); if (mainAxisIsHorizontal) { if (w.isFinite) contentWidth += w; } else { @@ -1107,18 +917,15 @@ class RenderFlexLayout extends RenderLayoutBox { } final bool mainAxisIsHorizontal = _isHorizontalFlexDirection; - final double gap = - _intrinsicMainAxisGap(mainAxisIsHorizontal: !mainAxisIsHorizontal); + final double gap = _intrinsicMainAxisGap(mainAxisIsHorizontal: !mainAxisIsHorizontal); double contentHeight = 0.0; int count = 0; RenderBox? child = firstChild; while (child != null) { - final RenderLayoutParentData childParentData = - child.parentData as RenderLayoutParentData; + final RenderLayoutParentData childParentData = child.parentData as RenderLayoutParentData; if (child is! RenderPositionPlaceholder) { - final double h = - child.getMinIntrinsicHeight(width) + _childMarginVertical(child); + final double h = child.getMinIntrinsicHeight(width) + _childMarginVertical(child); if (!mainAxisIsHorizontal) { if (h.isFinite) contentHeight += h; } else { @@ -1142,18 +949,15 @@ class RenderFlexLayout extends RenderLayoutBox { } final bool mainAxisIsHorizontal = _isHorizontalFlexDirection; - final double gap = - _intrinsicMainAxisGap(mainAxisIsHorizontal: !mainAxisIsHorizontal); + final double gap = _intrinsicMainAxisGap(mainAxisIsHorizontal: !mainAxisIsHorizontal); double contentHeight = 0.0; int count = 0; RenderBox? child = firstChild; while (child != null) { - final RenderLayoutParentData childParentData = - child.parentData as RenderLayoutParentData; + final RenderLayoutParentData childParentData = child.parentData as RenderLayoutParentData; if (child is! RenderPositionPlaceholder) { - final double h = - child.getMaxIntrinsicHeight(width) + _childMarginVertical(child); + final double h = child.getMaxIntrinsicHeight(width) + _childMarginVertical(child); if (!mainAxisIsHorizontal) { if (h.isFinite) contentHeight += h; } else { @@ -1179,13 +983,10 @@ class RenderFlexLayout extends RenderLayoutBox { // Unwrap wrappers to read padding/border from the flex item element itself. RenderBoxModel? box = child is RenderBoxModel ? child - : (child is RenderEventListener - ? child.child as RenderBoxModel? - : null); + : (child is RenderEventListener ? child.child as RenderBoxModel? : null); if (box == null) return basis; final double paddingBorder = _isHorizontalFlexDirection - ? (box.renderStyle.padding.horizontal + - box.renderStyle.border.horizontal) + ? (box.renderStyle.padding.horizontal + box.renderStyle.border.horizontal) : (box.renderStyle.padding.vertical + box.renderStyle.border.vertical); return math.max(basis, paddingBorder); } @@ -1196,15 +997,12 @@ class RenderFlexLayout extends RenderLayoutBox { // Cache the intrinsic size of children before flex-grow/flex-shrink // to avoid relayout when style of flex items changes. - Expando _childrenIntrinsicMainSizes = - Expando('childrenIntrinsicMainSizes'); + Expando _childrenIntrinsicMainSizes = Expando('childrenIntrinsicMainSizes'); // Cache original constraints of children on the first layout. - Expando _childrenOldConstraints = - Expando('childrenOldConstraints'); + Expando _childrenOldConstraints = Expando('childrenOldConstraints'); Expando<_FlexIntrinsicMeasurementCacheBucket> _childrenIntrinsicMeasureCache = - Expando<_FlexIntrinsicMeasurementCacheBucket>( - 'childrenIntrinsicMeasureCache'); + Expando<_FlexIntrinsicMeasurementCacheBucket>('childrenIntrinsicMeasureCache'); Expando _childrenRequirePostMeasureLayout = Expando('childrenRequirePostMeasureLayout'); Expando? _transientChildSizeOverrides; @@ -1221,8 +1019,7 @@ class RenderFlexLayout extends RenderLayoutBox { _childrenIntrinsicMainSizes = Expando('childrenIntrinsicMainSizes'); _childrenOldConstraints = Expando('childrenOldConstraints'); _childrenIntrinsicMeasureCache = - Expando<_FlexIntrinsicMeasurementCacheBucket>( - 'childrenIntrinsicMeasureCache'); + Expando<_FlexIntrinsicMeasurementCacheBucket>('childrenIntrinsicMeasureCache'); _childrenRequirePostMeasureLayout = Expando('childrenRequirePostMeasureLayout'); _transientChildSizeOverrides = null; @@ -1273,8 +1070,7 @@ class RenderFlexLayout extends RenderLayoutBox { switch (renderStyle.flexDirection) { case FlexDirection.row: if (inlineIsHorizontal) { - return dir != - TextDirection.rtl; // LTR → left is start; RTL → right is start + return dir != TextDirection.rtl; // LTR → left is start; RTL → right is start } else { return true; // vertical inline: top is start } @@ -1285,10 +1081,10 @@ class RenderFlexLayout extends RenderLayoutBox { return false; // vertical inline: bottom is start } case FlexDirection.column: - // Column follows block axis. - // - horizontal-tb: block is vertical (top is start) - // - vertical-rl: block is horizontal (start at physical right) - // - vertical-lr: block is horizontal (start at physical left) + // Column follows block axis. + // - horizontal-tb: block is vertical (top is start) + // - vertical-rl: block is horizontal (start at physical right) + // - vertical-lr: block is horizontal (start at physical left) if (inlineIsHorizontal) { return true; // top is start } else { @@ -1321,29 +1117,20 @@ class RenderFlexLayout extends RenderLayoutBox { // Get start/end padding in the main axis according to flex direction. double _flowAwareMainAxisPadding({bool isEnd = false}) { final _FlexContainerInvariants? inv = _layoutInvariants; - if (inv != null) - return isEnd ? inv.mainAxisPaddingEnd : inv.mainAxisPaddingStart; + if (inv != null) return isEnd ? inv.mainAxisPaddingEnd : inv.mainAxisPaddingStart; if (_isHorizontalFlexDirection) { final bool startIsLeft = _isMainAxisStartAtPhysicalStart(); if (!isEnd) { - return startIsLeft - ? renderStyle.paddingLeft.computedValue - : renderStyle.paddingRight.computedValue; + return startIsLeft ? renderStyle.paddingLeft.computedValue : renderStyle.paddingRight.computedValue; } else { - return startIsLeft - ? renderStyle.paddingRight.computedValue - : renderStyle.paddingLeft.computedValue; + return startIsLeft ? renderStyle.paddingRight.computedValue : renderStyle.paddingLeft.computedValue; } } else { final bool startIsTop = _isMainAxisStartAtPhysicalStart(); if (!isEnd) { - return startIsTop - ? renderStyle.paddingTop.computedValue - : renderStyle.paddingBottom.computedValue; + return startIsTop ? renderStyle.paddingTop.computedValue : renderStyle.paddingBottom.computedValue; } else { - return startIsTop - ? renderStyle.paddingBottom.computedValue - : renderStyle.paddingTop.computedValue; + return startIsTop ? renderStyle.paddingBottom.computedValue : renderStyle.paddingTop.computedValue; } } } @@ -1351,28 +1138,24 @@ class RenderFlexLayout extends RenderLayoutBox { // Get start/end padding in the cross axis according to flex direction. double _flowAwareCrossAxisPadding({bool isEnd = false}) { final _FlexContainerInvariants? inv = _layoutInvariants; - if (inv != null) - return isEnd ? inv.crossAxisPaddingEnd : inv.crossAxisPaddingStart; + if (inv != null) return isEnd ? inv.crossAxisPaddingEnd : inv.crossAxisPaddingStart; // Cross axis comes from block axis for row, inline axis for column final CSSWritingMode wm = renderStyle.writingMode; final bool inlineIsHorizontal = (wm == CSSWritingMode.horizontalTb); final bool crossIsHorizontal; bool crossStartIsPhysicalStart; // left for horizontal, top for vertical - if (renderStyle.flexDirection == FlexDirection.row || - renderStyle.flexDirection == FlexDirection.rowReverse) { + if (renderStyle.flexDirection == FlexDirection.row || renderStyle.flexDirection == FlexDirection.rowReverse) { crossIsHorizontal = !inlineIsHorizontal; // block axis if (crossIsHorizontal) { - crossStartIsPhysicalStart = (wm == - CSSWritingMode - .verticalLr); // start at left for vertical-lr, right for vertical-rl + crossStartIsPhysicalStart = + (wm == CSSWritingMode.verticalLr); // start at left for vertical-lr, right for vertical-rl } else { crossStartIsPhysicalStart = true; // top for horizontal-tb } } else { crossIsHorizontal = inlineIsHorizontal; // inline axis if (crossIsHorizontal) { - crossStartIsPhysicalStart = (renderStyle.direction != - TextDirection.rtl); // left if LTR, right if RTL + crossStartIsPhysicalStart = (renderStyle.direction != TextDirection.rtl); // left if LTR, right if RTL } else { crossStartIsPhysicalStart = true; // top in vertical writing modes } @@ -1389,17 +1172,14 @@ class RenderFlexLayout extends RenderLayoutBox { : renderStyle.paddingLeft.computedValue; } } else { - return isEnd - ? renderStyle.paddingBottom.computedValue - : renderStyle.paddingTop.computedValue; + return isEnd ? renderStyle.paddingBottom.computedValue : renderStyle.paddingTop.computedValue; } } // Get start/end border in the main axis according to flex direction. double _flowAwareMainAxisBorder({bool isEnd = false}) { final _FlexContainerInvariants? inv = _layoutInvariants; - if (inv != null) - return isEnd ? inv.mainAxisBorderEnd : inv.mainAxisBorderStart; + if (inv != null) return isEnd ? inv.mainAxisBorderEnd : inv.mainAxisBorderStart; if (_isHorizontalFlexDirection) { final bool startIsLeft = _isMainAxisStartAtPhysicalStart(); if (!isEnd) { @@ -1428,16 +1208,13 @@ class RenderFlexLayout extends RenderLayoutBox { // Get start/end border in the cross axis according to flex direction. double _flowAwareCrossAxisBorder({bool isEnd = false}) { final _FlexContainerInvariants? inv = _layoutInvariants; - if (inv != null) - return isEnd ? inv.crossAxisBorderEnd : inv.crossAxisBorderStart; + if (inv != null) return isEnd ? inv.crossAxisBorderEnd : inv.crossAxisBorderStart; final CSSWritingMode wm = renderStyle.writingMode; final bool crossIsHorizontal = !_isHorizontalFlexDirection; if (crossIsHorizontal) { - final bool usesBlockAxis = - renderStyle.flexDirection == FlexDirection.row || - renderStyle.flexDirection == FlexDirection.rowReverse; - final bool crossStartIsPhysicalLeft = - usesBlockAxis ? (wm == CSSWritingMode.verticalLr) : true; + final bool usesBlockAxis = renderStyle.flexDirection == FlexDirection.row || + renderStyle.flexDirection == FlexDirection.rowReverse; + final bool crossStartIsPhysicalLeft = usesBlockAxis ? (wm == CSSWritingMode.verticalLr) : true; if (!isEnd) { return crossStartIsPhysicalLeft ? renderStyle.effectiveBorderLeftWidth.computedValue @@ -1492,16 +1269,14 @@ class RenderFlexLayout extends RenderLayoutBox { } double _calculateMainAxisMarginForJustContentType(double margin) { - if (renderStyle.justifyContent == JustifyContent.spaceBetween && - margin < 0) { + if (renderStyle.justifyContent == JustifyContent.spaceBetween && margin < 0) { return margin / 2; } return margin; } // Get start/end margin of child in the cross axis according to flex direction. - double? _flowAwareChildCrossAxisMargin(RenderBox child, - {bool isEnd = false}) { + double? _flowAwareChildCrossAxisMargin(RenderBox child, {bool isEnd = false}) { RenderBoxModel? childRenderBoxModel; if (child is RenderBoxModel) { childRenderBoxModel = child; @@ -1515,9 +1290,8 @@ class RenderFlexLayout extends RenderLayoutBox { final CSSWritingMode wm = renderStyle.writingMode; final bool crossIsHorizontal = !_isHorizontalFlexDirection; if (crossIsHorizontal) { - final bool usesBlockAxis = - renderStyle.flexDirection == FlexDirection.row || - renderStyle.flexDirection == FlexDirection.rowReverse; + final bool usesBlockAxis = renderStyle.flexDirection == FlexDirection.row || + renderStyle.flexDirection == FlexDirection.rowReverse; // When the cross axis is horizontal, it can be either the block axis (in // vertical writing modes for row/row-reverse) or the inline axis (for // column/column-reverse in horizontal writing mode). Inline-start depends @@ -1559,9 +1333,7 @@ class RenderFlexLayout extends RenderLayoutBox { } RenderBoxModel? box = child is RenderBoxModel ? child - : (child is RenderEventListener - ? child.child as RenderBoxModel? - : null); + : (child is RenderEventListener ? child.child as RenderBoxModel? : null); return box != null ? box.renderStyle.flexGrow : 0.0; } @@ -1572,18 +1344,14 @@ class RenderFlexLayout extends RenderLayoutBox { } RenderBoxModel? box = child is RenderBoxModel ? child - : (child is RenderEventListener - ? child.child as RenderBoxModel? - : null); + : (child is RenderEventListener ? child.child as RenderBoxModel? : null); return box != null ? box.renderStyle.flexShrink : 0.0; } double? _getFlexBasis(RenderBox child) { RenderBoxModel? box = child is RenderBoxModel ? child - : (child is RenderEventListener - ? child.child as RenderBoxModel? - : null); + : (child is RenderEventListener ? child.child as RenderBoxModel? : null); if (box != null && box.renderStyle.flexBasis != CSSLengthValue.auto) { // flex-basis: content → base size is content-based; do not return a numeric value here if (box.renderStyle.flexBasis?.type == CSSLengthType.CONTENT) { @@ -1596,9 +1364,7 @@ class RenderFlexLayout extends RenderLayoutBox { /// and if that containing block’s size is indefinite, the used value for flex-basis is content. // Note: When flex-basis is 0%, it should remain 0, not be changed to minContentWidth // The commented code below was incorrectly setting flexBasis to minContentWidth for 0% values - if (flexBasis != null && - flexBasis == 0 && - box.renderStyle.flexBasis?.type == CSSLengthType.PERCENTAGE) { + if (flexBasis != null && flexBasis == 0 && box.renderStyle.flexBasis?.type == CSSLengthType.PERCENTAGE) { // CSS Flexbox: percentage flex-basis is resolved against the flex container’s // inner main size. If that size is indefinite, the used value is 'content'. // Consider explicit sizing, tight constraints, or bounded constraints as definite. @@ -1606,10 +1372,8 @@ class RenderFlexLayout extends RenderLayoutBox { ? (renderStyle.contentBoxLogicalWidth != null) : (renderStyle.contentBoxLogicalHeight != null); final bool mainTight = _isHorizontalFlexDirection - ? ((contentConstraints?.hasTightWidth ?? false) || - constraints.hasTightWidth) - : ((contentConstraints?.hasTightHeight ?? false) || - constraints.hasTightHeight); + ? ((contentConstraints?.hasTightWidth ?? false) || constraints.hasTightWidth) + : ((contentConstraints?.hasTightHeight ?? false) || constraints.hasTightHeight); final bool mainDefinite = hasSpecifiedMain || mainTight; if (!mainDefinite) { return null; @@ -1636,18 +1400,15 @@ class RenderFlexLayout extends RenderLayoutBox { double _getMaxMainAxisSize(RenderBoxModel child) { double? resolvePctCap(CSSLengthValue len) { - if (len.type != CSSLengthType.PERCENTAGE || len.value == null) - return null; + if (len.type != CSSLengthType.PERCENTAGE || len.value == null) return null; // Determine the container's inner content-box size in the main axis. double? containerInner; if (_isHorizontalFlexDirection) { containerInner = renderStyle.contentBoxLogicalWidth; if (containerInner == null) { - if (contentConstraints != null && - contentConstraints!.maxWidth.isFinite) { + if (contentConstraints != null && contentConstraints!.maxWidth.isFinite) { containerInner = contentConstraints!.maxWidth; - } else if (constraints.hasTightWidth && - constraints.maxWidth.isFinite) { + } else if (constraints.hasTightWidth && constraints.maxWidth.isFinite) { containerInner = constraints.maxWidth; } else { // Fallback to ancestor-provided available inline size used for shrink-to-fit. @@ -1658,11 +1419,9 @@ class RenderFlexLayout extends RenderLayoutBox { } else { containerInner = renderStyle.contentBoxLogicalHeight; if (containerInner == null) { - if (contentConstraints != null && - contentConstraints!.maxHeight.isFinite) { + if (contentConstraints != null && contentConstraints!.maxHeight.isFinite) { containerInner = contentConstraints!.maxHeight; - } else if (constraints.hasTightHeight && - constraints.maxHeight.isFinite) { + } else if (constraints.hasTightHeight && constraints.maxHeight.isFinite) { containerInner = constraints.maxHeight; } } @@ -1696,13 +1455,9 @@ class RenderFlexLayout extends RenderLayoutBox { if (child is RenderBoxModel) { double autoMinSize = _getAutoMinSize(child); if (_isHorizontalFlexDirection) { - minMainSize = child.renderStyle.minWidth.isAuto - ? autoMinSize - : child.renderStyle.minWidth.computedValue; + minMainSize = child.renderStyle.minWidth.isAuto ? autoMinSize : child.renderStyle.minWidth.computedValue; } else { - minMainSize = child.renderStyle.minHeight.isAuto - ? autoMinSize - : child.renderStyle.minHeight.computedValue; + minMainSize = child.renderStyle.minHeight.isAuto ? autoMinSize : child.renderStyle.minHeight.computedValue; } } @@ -1718,13 +1473,11 @@ class RenderFlexLayout extends RenderLayoutBox { if (child is RenderTextBox) { // RenderEventListener directly contains a text box - mark it for flex relayout _setFlexRelayoutForTextParent(boxModel); - } else if (child is RenderLayoutBox && - _hasOnlyTextFlexRelayoutSubtree(child)) { + } else if (child is RenderLayoutBox) { // RenderEventListener contains a layout box - check if that layout box only contains text _markRenderLayoutBoxForTextOnly(child); } - } else if (boxModel is RenderLayoutBox && - _hasOnlyTextFlexRelayoutSubtree(boxModel)) { + } else if (boxModel is RenderLayoutBox) { // Check if this layout box only contains text _markRenderLayoutBoxForTextOnly(boxModel); } @@ -1735,10 +1488,6 @@ class RenderFlexLayout extends RenderLayoutBox { // preventing constraint violations when the flex container adjusts item sizes. // Only apply when the flex container itself has indefinite width. void _markRenderLayoutBoxForTextOnly(RenderLayoutBox layoutBox) { - if (!_hasOnlyTextFlexRelayoutSubtree(layoutBox)) { - return; - } - if (layoutBox.childCount == 1) { RenderObject? firstChild = layoutBox.firstChild; if (firstChild is RenderEventListener) { @@ -1754,25 +1503,6 @@ class RenderFlexLayout extends RenderLayoutBox { } } - bool _hasOnlyTextFlexRelayoutSubtree(RenderObject? node) { - if (node == null) { - return false; - } - if (node is RenderTextBox) { - return true; - } - if (node is RenderEventListener) { - return _hasOnlyTextFlexRelayoutSubtree(node.child); - } - if (node is RenderLayoutBox) { - if (node.childCount != 1) { - return false; - } - return _hasOnlyTextFlexRelayoutSubtree(node.firstChild); - } - return false; - } - void _setFlexRelayoutForTextParent(RenderBoxModel textParentBoxModel) { if (textParentBoxModel.renderStyle.display == CSSDisplay.flex && textParentBoxModel.renderStyle.width.isAuto && @@ -1803,16 +1533,13 @@ class RenderFlexLayout extends RenderLayoutBox { // (clamped by its max main size property if it’s definite). It is otherwise undefined. // https://www.w3.org/TR/css-flexbox-1/#specified-size-suggestion double? specifiedSize; - final CSSLengthValue mainSize = _isHorizontalFlexDirection - ? childRenderStyle.width - : childRenderStyle.height; + final CSSLengthValue mainSize = _isHorizontalFlexDirection ? childRenderStyle.width : childRenderStyle.height; if (!mainSize.isIntrinsic && mainSize.isNotAuto) { if (mainSize.type == CSSLengthType.PERCENTAGE) { // Percentage main sizes resolve against the flex container and may be // indefinite until layout. Use the already-resolved logical size when // available. - specifiedSize = - _isHorizontalFlexDirection ? childLogicalWidth : childLogicalHeight; + specifiedSize = _isHorizontalFlexDirection ? childLogicalWidth : childLogicalHeight; } else { // Avoid using `contentBoxLogicalWidth/Height` here: those values can be // overridden by flex sizing (grow/shrink) and would make the "specified @@ -1823,8 +1550,7 @@ class RenderFlexLayout extends RenderLayoutBox { double contentBoxMain = _isHorizontalFlexDirection ? childRenderStyle.deflatePaddingBorderWidth(borderBoxMain) : childRenderStyle.deflatePaddingBorderHeight(borderBoxMain); - if (!contentBoxMain.isFinite || contentBoxMain < 0) - contentBoxMain = 0; + if (!contentBoxMain.isFinite || contentBoxMain < 0) contentBoxMain = 0; specifiedSize = contentBoxMain; } } @@ -1856,10 +1582,8 @@ class RenderFlexLayout extends RenderLayoutBox { double contentSize; if (_isHorizontalFlexDirection) { if (child is RenderFlowLayout && child.inlineFormattingContext != null) { - final double ifcMin = - child.inlineFormattingContext!.paragraphMinIntrinsicWidth; - contentSize = - (ifcMin.isFinite && ifcMin > 0) ? ifcMin : child.minContentWidth; + final double ifcMin = child.inlineFormattingContext!.paragraphMinIntrinsicWidth; + contentSize = (ifcMin.isFinite && ifcMin > 0) ? ifcMin : child.minContentWidth; } else { contentSize = child.minContentWidth; } @@ -1874,9 +1598,7 @@ class RenderFlexLayout extends RenderLayoutBox { } } - CSSLengthValue childCrossSize = _isHorizontalFlexDirection - ? childRenderStyle.height - : childRenderStyle.width; + CSSLengthValue childCrossSize = _isHorizontalFlexDirection ? childRenderStyle.height : childRenderStyle.width; if (childCrossSize.isNotAuto && transferredSize != null) { contentSize = transferredSize; @@ -1889,19 +1611,15 @@ class RenderFlexLayout extends RenderLayoutBox { if (childAspectRatio != null) { if (_isHorizontalFlexDirection) { if (childRenderStyle.minHeight.isNotAuto) { - transferredMinSize = - childRenderStyle.minHeight.computedValue * childAspectRatio; + transferredMinSize = childRenderStyle.minHeight.computedValue * childAspectRatio; } else if (childRenderStyle.maxHeight.isNotNone) { - transferredMaxSize = - childRenderStyle.maxHeight.computedValue * childAspectRatio; + transferredMaxSize = childRenderStyle.maxHeight.computedValue * childAspectRatio; } } else if (!_isHorizontalFlexDirection) { if (childRenderStyle.minWidth.isNotAuto) { - transferredMinSize = - childRenderStyle.minWidth.computedValue / childAspectRatio; + transferredMinSize = childRenderStyle.minWidth.computedValue / childAspectRatio; } else if (childRenderStyle.maxWidth.isNotNone) { - transferredMaxSize = - childRenderStyle.maxWidth.computedValue * childAspectRatio; + transferredMaxSize = childRenderStyle.maxWidth.computedValue * childAspectRatio; } } } @@ -1914,23 +1632,19 @@ class RenderFlexLayout extends RenderLayoutBox { contentSize = transferredMaxSize; } - double? crossSize = _isHorizontalFlexDirection - ? renderStyle.contentBoxLogicalHeight - : renderStyle.contentBoxLogicalWidth; + double? crossSize = + _isHorizontalFlexDirection ? renderStyle.contentBoxLogicalHeight : renderStyle.contentBoxLogicalWidth; // Content size suggestion of replaced flex item will use the cross axis preferred size which came from flexbox's // fixed cross size in newer version of Blink and Gecko which is different from the behavior of WebKit. // https://github.com/w3c/csswg-drafts/issues/6693 - bool isChildCrossSizeStretched = - _needToStretchChildCrossSize(child) && crossSize != null; + bool isChildCrossSizeStretched = _needToStretchChildCrossSize(child) && crossSize != null; if (isChildCrossSizeStretched && transferredSize != null) { contentSize = transferredSize; } - CSSLengthValue maxMainLength = _isHorizontalFlexDirection - ? childRenderStyle.maxWidth - : childRenderStyle.maxHeight; + CSSLengthValue maxMainLength = _isHorizontalFlexDirection ? childRenderStyle.maxWidth : childRenderStyle.maxHeight; // Further clamped by the max main size property if that is definite. if (maxMainLength.isNotNone) { @@ -1962,22 +1676,17 @@ class RenderFlexLayout extends RenderLayoutBox { // Convert the content-box minimum to a border-box minimum by adding padding and border // on the flex container's main axis, per CSS sizing. final double paddingBorderMain = _isHorizontalFlexDirection - ? (childRenderStyle.padding.horizontal + - childRenderStyle.border.horizontal) - : (childRenderStyle.padding.vertical + - childRenderStyle.border.vertical); + ? (childRenderStyle.padding.horizontal + childRenderStyle.border.horizontal) + : (childRenderStyle.padding.vertical + childRenderStyle.border.vertical); // If overflow in the flex main axis is not visible, browsers allow the flex item // to shrink below the content-based minimum. Model this by treating the automatic // minimum as zero (border-box becomes just padding+border). - if ((_isHorizontalFlexDirection && - childRenderStyle.overflowX != CSSOverflowType.visible) || - (!_isHorizontalFlexDirection && - childRenderStyle.overflowY != CSSOverflowType.visible)) { + if ((_isHorizontalFlexDirection && childRenderStyle.overflowX != CSSOverflowType.visible) || + (!_isHorizontalFlexDirection && childRenderStyle.overflowY != CSSOverflowType.visible)) { double autoMinBorderBox = paddingBorderMain; if (maxMainLength.isNotNone) { - autoMinBorderBox = - math.min(autoMinBorderBox, maxMainLength.computedValue); + autoMinBorderBox = math.min(autoMinBorderBox, maxMainLength.computedValue); } return autoMinBorderBox; } @@ -1986,8 +1695,7 @@ class RenderFlexLayout extends RenderLayoutBox { // Finally, clamp by the definite max main size (which is border-box) if present. if (maxMainLength.isNotNone) { - autoMinBorderBox = - math.min(autoMinBorderBox, maxMainLength.computedValue); + autoMinBorderBox = math.min(autoMinBorderBox, maxMainLength.computedValue); } return autoMinBorderBox; @@ -2008,11 +1716,9 @@ class RenderFlexLayout extends RenderLayoutBox { child = child.child as RenderBoxModel; } if (_isPlaceholderPositioned(child)) { - RenderBoxModel? positionedBox = - (child as RenderPositionPlaceholder).positioned; + RenderBoxModel? positionedBox = (child as RenderPositionPlaceholder).positioned; if (positionedBox != null && positionedBox.hasSize == true) { - Size realDisplayedBoxSize = - positionedBox.getBoxSize(positionedBox.contentSize); + Size realDisplayedBoxSize = positionedBox.getBoxSize(positionedBox.contentSize); return BoxConstraints( minWidth: realDisplayedBoxSize.width, maxWidth: realDisplayedBoxSize.width, @@ -2045,8 +1751,7 @@ class RenderFlexLayout extends RenderLayoutBox { // This prevents items from measuring at full container width/height. // Exception: replaced elements (e.g., ) should not be relaxed to ∞, // otherwise they pick viewport-sized widths; keep their container-bounded constraints. - final bool isFlexBasisContent = - s.flexBasis?.type == CSSLengthType.CONTENT; + final bool isFlexBasisContent = s.flexBasis?.type == CSSLengthType.CONTENT; final bool isReplaced = s.isSelfRenderReplaced(); if (_isHorizontalFlexDirection) { // Row direction: main axis is width. For intrinsic measurement, avoid @@ -2055,12 +1760,12 @@ class RenderFlexLayout extends RenderLayoutBox { if (!isReplaced && (s.width.isAuto || isFlexBasisContent)) { // Relax the minimum width to the element's own border-box minimum, // not the parent-imposed tight width, so shrink-to-fit can occur. - double minBorderBoxW = s.effectiveBorderLeftWidth.computedValue + - s.effectiveBorderRightWidth.computedValue + - s.paddingLeft.computedValue + - s.paddingRight.computedValue; - if (s.minWidth.isNotAuto && - s.minWidth.type != CSSLengthType.PERCENTAGE) { + double minBorderBoxW = + s.effectiveBorderLeftWidth.computedValue + + s.effectiveBorderRightWidth.computedValue + + s.paddingLeft.computedValue + + s.paddingRight.computedValue; + if (s.minWidth.isNotAuto && s.minWidth.type != CSSLengthType.PERCENTAGE) { minBorderBoxW = math.max(minBorderBoxW, s.minWidth.computedValue); } c = BoxConstraints( @@ -2076,12 +1781,12 @@ class RenderFlexLayout extends RenderLayoutBox { // when the item has auto height or flex-basis:content. This lets the // item size to its content instead of being prematurely clamped to 0. if (!isReplaced && (s.height.isAuto || isFlexBasisContent)) { - double minBorderBoxH = s.effectiveBorderTopWidth.computedValue + - s.effectiveBorderBottomWidth.computedValue + - s.paddingTop.computedValue + - s.paddingBottom.computedValue; - if (s.minHeight.isNotAuto && - s.minHeight.type != CSSLengthType.PERCENTAGE) { + double minBorderBoxH = + s.effectiveBorderTopWidth.computedValue + + s.effectiveBorderBottomWidth.computedValue + + s.paddingTop.computedValue + + s.paddingBottom.computedValue; + if (s.minHeight.isNotAuto && s.minHeight.type != CSSLengthType.PERCENTAGE) { minBorderBoxH = math.max(minBorderBoxH, s.minHeight.computedValue); } c = BoxConstraints( @@ -2099,11 +1804,8 @@ class RenderFlexLayout extends RenderLayoutBox { if (!isReplaced && (s.width.isAuto || isFlexBasisContent)) { // Determine if child should be stretched in cross axis. final AlignSelf self = s.alignSelf; - final bool parentStretch = - renderStyle.alignItems == AlignItems.stretch; - final bool shouldStretch = self == AlignSelf.auto - ? parentStretch - : self == AlignSelf.stretch; + final bool parentStretch = renderStyle.alignItems == AlignItems.stretch; + final bool shouldStretch = self == AlignSelf.auto ? parentStretch : self == AlignSelf.stretch; // Determine if the container has a definite cross size (width). // Determine whether the flex container's cross size (width in column direction) @@ -2114,14 +1816,12 @@ class RenderFlexLayout extends RenderLayoutBox { // container is not inline-flex with auto width (inline-flex shrink-to-fit should // be treated as indefinite during intrinsic measurement). final bool isInlineFlexAuto = - renderStyle.effectiveDisplay == CSSDisplay.inlineFlex && - renderStyle.width.isAuto; + renderStyle.effectiveDisplay == CSSDisplay.inlineFlex && renderStyle.width.isAuto; final bool containerCrossDefinite = (renderStyle.contentBoxLogicalWidth != null) || (contentConstraints?.hasTightWidth ?? false) || (constraints.hasTightWidth && !isInlineFlexAuto) || - (((contentConstraints?.hasBoundedWidth ?? false) || - constraints.hasBoundedWidth) && + (((contentConstraints?.hasBoundedWidth ?? false) || constraints.hasBoundedWidth) && !isInlineFlexAuto); double newMaxW; @@ -2131,13 +1831,11 @@ class RenderFlexLayout extends RenderLayoutBox { double boundedContainerW = double.infinity; if (constraints.hasTightWidth && constraints.maxWidth.isFinite) { boundedContainerW = constraints.maxWidth; - } else if ((contentConstraints?.hasTightWidth ?? false) && - contentConstraints!.maxWidth.isFinite) { + } else if ((contentConstraints?.hasTightWidth ?? false) && contentConstraints!.maxWidth.isFinite) { boundedContainerW = contentConstraints!.maxWidth; } else if (!isInlineFlexAuto) { // Fall back to bounded (non-tight) width for block-level flex containers. - if ((contentConstraints?.hasBoundedWidth ?? false) && - contentConstraints!.maxWidth.isFinite) { + if ((contentConstraints?.hasBoundedWidth ?? false) && contentConstraints!.maxWidth.isFinite) { boundedContainerW = contentConstraints!.maxWidth; } } @@ -2154,8 +1852,7 @@ class RenderFlexLayout extends RenderLayoutBox { newMaxW = double.infinity; } // Also honor the child's own definite max-width (non-percentage) if any. - if (s.maxWidth.isNotNone && - s.maxWidth.type != CSSLengthType.PERCENTAGE) { + if (s.maxWidth.isNotNone && s.maxWidth.type != CSSLengthType.PERCENTAGE) { newMaxW = math.min(newMaxW, s.maxWidth.computedValue); } @@ -2190,9 +1887,7 @@ class RenderFlexLayout extends RenderLayoutBox { // when that size is effectively indefinite for intrinsic measurement, using 0 // would prematurely collapse the child. Let content sizing drive the measure. final RenderBoxModel childBox = child; - final bool isZeroPctBasis = - childBox.renderStyle.flexBasis?.type == CSSLengthType.PERCENTAGE && - basis == 0; + final bool isZeroPctBasis = childBox.renderStyle.flexBasis?.type == CSSLengthType.PERCENTAGE && basis == 0; // Only skip clamping to 0 for percentage flex-basis during intrinsic sizing // in column direction (vertical main axis). In row direction, a 0% flex-basis // should remain 0 for intrinsic measurement so width-based distribution matches CSS. @@ -2202,8 +1897,8 @@ class RenderFlexLayout extends RenderLayoutBox { // Flex-basis is a definite length. Honor box-sizing:border-box semantics: // the used border-box size cannot be smaller than padding+border. if (_isHorizontalFlexDirection) { - final double minBorderBoxW = child.renderStyle.padding.horizontal + - child.renderStyle.border.horizontal; + final double minBorderBoxW = + child.renderStyle.padding.horizontal + child.renderStyle.border.horizontal; final double used = math.max(basis, minBorderBoxW); c = BoxConstraints( minWidth: used, @@ -2212,8 +1907,8 @@ class RenderFlexLayout extends RenderLayoutBox { maxHeight: c.maxHeight, ); } else { - final double minBorderBoxH = child.renderStyle.padding.vertical + - child.renderStyle.border.vertical; + final double minBorderBoxH = + child.renderStyle.padding.vertical + child.renderStyle.border.vertical; final double used = math.max(basis, minBorderBoxH); c = BoxConstraints( minWidth: c.minWidth, @@ -2245,9 +1940,8 @@ class RenderFlexLayout extends RenderLayoutBox { } if (childRenderBoxModel != null) { - marginHorizontal = - childRenderBoxModel.renderStyle.marginLeft.computedValue + - childRenderBoxModel.renderStyle.marginRight.computedValue; + marginHorizontal = childRenderBoxModel.renderStyle.marginLeft.computedValue + + childRenderBoxModel.renderStyle.marginRight.computedValue; marginVertical = childRenderBoxModel.renderStyle.marginTop.computedValue + childRenderBoxModel.renderStyle.marginBottom.computedValue; } @@ -2264,8 +1958,7 @@ class RenderFlexLayout extends RenderLayoutBox { } } - double _horizontalMarginNegativeSet(double baseSize, RenderBoxModel box, - {bool isHorizontal = false}) { + double _horizontalMarginNegativeSet(double baseSize, RenderBoxModel box, {bool isHorizontal = false}) { CSSRenderStyle boxStyle = box.renderStyle; double? marginLeft = boxStyle.marginLeft.computedValue; double? marginRight = boxStyle.marginRight.computedValue; @@ -2287,8 +1980,7 @@ class RenderFlexLayout extends RenderLayoutBox { return baseSize + box.renderStyle.margin.vertical; } - double _getMainAxisExtent(RenderBox child, - {bool shouldUseIntrinsicMainSize = false}) { + double _getMainAxisExtent(RenderBox child, {bool shouldUseIntrinsicMainSize = false}) { double marginHorizontal = 0; double marginVertical = 0; @@ -2302,15 +1994,13 @@ class RenderFlexLayout extends RenderLayoutBox { } if (childRenderBoxModel != null) { - marginHorizontal = - childRenderBoxModel.renderStyle.marginLeft.computedValue + - childRenderBoxModel.renderStyle.marginRight.computedValue; + marginHorizontal = childRenderBoxModel.renderStyle.marginLeft.computedValue + + childRenderBoxModel.renderStyle.marginRight.computedValue; marginVertical = childRenderBoxModel.renderStyle.marginTop.computedValue + childRenderBoxModel.renderStyle.marginBottom.computedValue; } - double baseSize = _getMainSize(child, - shouldUseIntrinsicMainSize: shouldUseIntrinsicMainSize); + double baseSize = _getMainSize(child, shouldUseIntrinsicMainSize: shouldUseIntrinsicMainSize); if (_isHorizontalFlexDirection) { if (child is RenderLayoutBox && child.isNegativeMarginChangeHSize) { return _horizontalMarginNegativeSet(baseSize, child); @@ -2321,10 +2011,8 @@ class RenderFlexLayout extends RenderLayoutBox { } } - double _getMainSize(RenderBox child, - {bool shouldUseIntrinsicMainSize = false}) { - Size? childSize = _getChildSize(child, - shouldUseIntrinsicMainSize: shouldUseIntrinsicMainSize); + double _getMainSize(RenderBox child, {bool shouldUseIntrinsicMainSize = false}) { + Size? childSize = _getChildSize(child, shouldUseIntrinsicMainSize: shouldUseIntrinsicMainSize); if (_isHorizontalFlexDirection) { return childSize!.width; } else { @@ -2336,8 +2024,9 @@ class RenderFlexLayout extends RenderLayoutBox { double _getMainAxisGap() { final _FlexContainerInvariants? inv = _layoutInvariants; if (inv != null) return inv.mainAxisGap; - CSSLengthValue gap = - _isHorizontalFlexDirection ? renderStyle.columnGap : renderStyle.rowGap; + CSSLengthValue gap = _isHorizontalFlexDirection + ? renderStyle.columnGap + : renderStyle.rowGap; if (gap.type == CSSLengthType.NORMAL) return 0; return gap.computedValue; } @@ -2346,8 +2035,9 @@ class RenderFlexLayout extends RenderLayoutBox { double _getCrossAxisGap() { final _FlexContainerInvariants? inv = _layoutInvariants; if (inv != null) return inv.crossAxisGap; - CSSLengthValue gap = - _isHorizontalFlexDirection ? renderStyle.rowGap : renderStyle.columnGap; + CSSLengthValue gap = _isHorizontalFlexDirection + ? renderStyle.rowGap + : renderStyle.columnGap; if (gap.type == CSSLengthType.NORMAL) return 0; return gap.computedValue; } @@ -2387,12 +2077,9 @@ class RenderFlexLayout extends RenderLayoutBox { ); items.sort((_OrderedFlexItem a, _OrderedFlexItem b) { final int byOrder = a.order.compareTo(b.order); - return byOrder != 0 - ? byOrder - : a.originalIndex.compareTo(b.originalIndex); + return byOrder != 0 ? byOrder : a.originalIndex.compareTo(b.originalIndex); }); - return List.generate(items.length, (int i) => items[i].child, - growable: false); + return List.generate(items.length, (int i) => items[i].child, growable: false); } _FlexResolutionInputs _computeFlexResolutionInputs() { @@ -2409,28 +2096,24 @@ class RenderFlexLayout extends RenderLayoutBox { double? containerHeight; if (containerWidth == null) { - if ((contentConstraints?.hasBoundedWidth ?? false) && - (contentConstraints?.maxWidth.isFinite ?? false)) { + if ((contentConstraints?.hasBoundedWidth ?? false) && (contentConstraints?.maxWidth.isFinite ?? false)) { containerWidth = contentConstraints!.maxWidth; } else if (constraints.hasBoundedWidth && constraints.maxWidth.isFinite) { containerWidth = constraints.maxWidth; } } if (constraints.hasBoundedWidth && constraints.maxWidth.isFinite) { - containerWidth = (containerWidth == null) - ? constraints.maxWidth - : math.min(containerWidth, constraints.maxWidth); + containerWidth = + (containerWidth == null) ? constraints.maxWidth : math.min(containerWidth, constraints.maxWidth); } - if ((contentConstraints?.hasBoundedHeight ?? false) && - (contentConstraints?.maxHeight.isFinite ?? false)) { + if ((contentConstraints?.hasBoundedHeight ?? false) && (contentConstraints?.maxHeight.isFinite ?? false)) { containerHeight = contentConstraints!.maxHeight; } else if (constraints.hasBoundedHeight && constraints.maxHeight.isFinite) { containerHeight = constraints.maxHeight; } if (constraints.hasBoundedHeight && constraints.maxHeight.isFinite) { - containerHeight = (containerHeight == null) - ? constraints.maxHeight - : math.min(containerHeight, constraints.maxHeight); + containerHeight = + (containerHeight == null) ? constraints.maxHeight : math.min(containerHeight, constraints.maxHeight); } if (contentBoxLogicalHeight != null) { containerHeight = contentBoxLogicalHeight; @@ -2440,18 +2123,14 @@ class RenderFlexLayout extends RenderLayoutBox { final double? maxMainSize = isHorizontal ? containerWidth : containerHeight; final bool isMainSizeDefinite = isHorizontal - ? (contentBoxLogicalWidth != null || - (contentConstraints?.hasTightWidth ?? false) || - constraints.hasTightWidth || - ((contentConstraints?.hasBoundedWidth ?? false) && - (contentConstraints?.maxWidth.isFinite ?? false)) || - (constraints.hasBoundedWidth && constraints.maxWidth.isFinite)) - : (contentBoxLogicalHeight != null || - (contentConstraints?.hasTightHeight ?? false) || - constraints.hasTightHeight || - ((contentConstraints?.hasBoundedHeight ?? false) && - (contentConstraints?.maxHeight.isFinite ?? false)) || - (constraints.hasBoundedHeight && constraints.maxHeight.isFinite)); + ? (contentBoxLogicalWidth != null || (contentConstraints?.hasTightWidth ?? false) || + constraints.hasTightWidth || + ((contentConstraints?.hasBoundedWidth ?? false) && (contentConstraints?.maxWidth.isFinite ?? false)) || + (constraints.hasBoundedWidth && constraints.maxWidth.isFinite)) + : (contentBoxLogicalHeight != null || (contentConstraints?.hasTightHeight ?? false) || + constraints.hasTightHeight || + ((contentConstraints?.hasBoundedHeight ?? false) && (contentConstraints?.maxHeight.isFinite ?? false)) || + (constraints.hasBoundedHeight && constraints.maxHeight.isFinite)); return _FlexResolutionInputs( contentBoxLogicalWidth: contentBoxLogicalWidth, @@ -2462,22 +2141,17 @@ class RenderFlexLayout extends RenderLayoutBox { } _RunChild _createRunChildMetadata(RenderBox child, double originalMainSize, - {required RenderBoxModel? effectiveChild, - required double? usedFlexBasis}) { + {required RenderBoxModel? effectiveChild, required double? usedFlexBasis}) { double mainAxisMargin = 0.0; if (effectiveChild != null) { final RenderStyle s = effectiveChild.renderStyle; - final double marginHorizontal = - s.marginLeft.computedValue + s.marginRight.computedValue; - final double marginVertical = - s.marginTop.computedValue + s.marginBottom.computedValue; - mainAxisMargin = - _isHorizontalFlexDirection ? marginHorizontal : marginVertical; + final double marginHorizontal = s.marginLeft.computedValue + s.marginRight.computedValue; + final double marginVertical = s.marginTop.computedValue + s.marginBottom.computedValue; + mainAxisMargin = _isHorizontalFlexDirection ? marginHorizontal : marginVertical; } - final RenderBoxModel? marginBoxModel = child is RenderBoxModel - ? child - : (child is RenderPositionPlaceholder ? child.positioned : null); + final RenderBoxModel? marginBoxModel = + child is RenderBoxModel ? child : (child is RenderPositionPlaceholder ? child.positioned : null); final RenderStyle? marginStyle = marginBoxModel?.renderStyle; final bool marginLeftAuto = marginStyle?.marginLeft.isAuto ?? false; final bool marginRightAuto = marginStyle?.marginRight.isAuto ?? false; @@ -2505,11 +2179,9 @@ class RenderFlexLayout extends RenderLayoutBox { usedFlexBasis: usedFlexBasis, mainAxisMargin: mainAxisMargin, mainAxisStartMargin: _flowAwareChildMainAxisMargin(child) ?? 0.0, - mainAxisEndMargin: - _flowAwareChildMainAxisMargin(child, isEnd: true) ?? 0.0, + mainAxisEndMargin: _flowAwareChildMainAxisMargin(child, isEnd: true) ?? 0.0, crossAxisStartMargin: _flowAwareChildCrossAxisMargin(child) ?? 0.0, - crossAxisEndMargin: - _flowAwareChildCrossAxisMargin(child, isEnd: true) ?? 0.0, + crossAxisEndMargin: _flowAwareChildCrossAxisMargin(child, isEnd: true) ?? 0.0, hasAutoMainAxisMargin: hasAutoMainAxisMargin, hasAutoCrossAxisMargin: hasAutoCrossAxisMargin, marginLeftAuto: marginLeftAuto, @@ -2521,19 +2193,14 @@ class RenderFlexLayout extends RenderLayoutBox { ); } - void _cacheOriginalConstraintsIfNeeded( - RenderBox child, BoxConstraints appliedConstraints) { + void _cacheOriginalConstraintsIfNeeded(RenderBox child, BoxConstraints appliedConstraints) { RenderBoxModel? box = child is RenderBoxModel ? child - : (child is RenderEventListener - ? child.child as RenderBoxModel? - : null); + : (child is RenderEventListener ? child.child as RenderBoxModel? : null); if (box == null) return; - bool hasPercentageMaxWidth = - box.renderStyle.maxWidth.type == CSSLengthType.PERCENTAGE; - bool hasPercentageMaxHeight = - box.renderStyle.maxHeight.type == CSSLengthType.PERCENTAGE; + bool hasPercentageMaxWidth = box.renderStyle.maxWidth.type == CSSLengthType.PERCENTAGE; + bool hasPercentageMaxHeight = box.renderStyle.maxHeight.type == CSSLengthType.PERCENTAGE; if (hasPercentageMaxWidth || hasPercentageMaxHeight) { _childrenOldConstraints[box] = appliedConstraints; @@ -2628,8 +2295,7 @@ class RenderFlexLayout extends RenderLayoutBox { return true; } final AlignSelf alignSelf = _getAlignSelf(child); - return alignSelf == AlignSelf.baseline || - alignSelf == AlignSelf.lastBaseline; + return alignSelf == AlignSelf.baseline || alignSelf == AlignSelf.lastBaseline; } bool _subtreeHasPendingIntrinsicMeasureInvalidation(RenderBox root) { @@ -2641,8 +2307,7 @@ class RenderFlexLayout extends RenderLayoutBox { return true; } - if (root is ContainerRenderObjectMixin>) { + if (root is ContainerRenderObjectMixin>) { RenderBox? child = (root as dynamic).firstChild as RenderBox?; while (child != null) { if (_subtreeHasPendingIntrinsicMeasureInvalidation(child)) { @@ -2695,8 +2360,7 @@ class RenderFlexLayout extends RenderLayoutBox { }; } - if (root is ContainerRenderObjectMixin>) { + if (root is ContainerRenderObjectMixin>) { RenderBox? child = (root as dynamic).firstChild as RenderBox?; while (child != null) { final Map? details = @@ -2726,8 +2390,8 @@ class RenderFlexLayout extends RenderLayoutBox { root.clearIntrinsicMeasurementInvalidationAfterMeasurement(); } - if (root is ContainerRenderObjectMixin>) { + if (root + is ContainerRenderObjectMixin>) { RenderBox? child = (root as dynamic).firstChild as RenderBox?; while (child != null) { _clearSubtreeIntrinsicMeasurementInvalidationAfterMeasurement(child); @@ -2772,7 +2436,8 @@ class RenderFlexLayout extends RenderLayoutBox { BoxConstraints childConstraints, { RenderFlowLayout? flowChild, }) { - final RenderFlowLayout? effectiveFlowChild = flowChild ?? + final RenderFlowLayout? effectiveFlowChild = + flowChild ?? _getCacheableIntrinsicMeasureFlowChild( child, allowAnonymous: true, @@ -2783,8 +2448,7 @@ class RenderFlexLayout extends RenderLayoutBox { int hash = 0; final CSSRenderStyle style = effectiveFlowChild.renderStyle; - hash = _hashReusableIntrinsicMeasurementState( - hash, effectiveFlowChild.hashCode); + hash = _hashReusableIntrinsicMeasurementState(hash, effectiveFlowChild.hashCode); hash = _hashReusableIntrinsicMeasurementState( hash, _quantizeReusableIntrinsicMeasurementDouble(childConstraints.minWidth), @@ -2802,16 +2466,11 @@ class RenderFlexLayout extends RenderLayoutBox { _quantizeReusableIntrinsicMeasurementDouble(childConstraints.maxHeight), ); hash = _hashReusableIntrinsicMeasurementState(hash, style.display.hashCode); - hash = - _hashReusableIntrinsicMeasurementState(hash, style.position.hashCode); - hash = - _hashReusableIntrinsicMeasurementState(hash, style.whiteSpace.hashCode); - hash = - _hashReusableIntrinsicMeasurementState(hash, style.wordBreak.hashCode); - hash = - _hashReusableIntrinsicMeasurementState(hash, style.textAlign.hashCode); - hash = - _hashReusableIntrinsicMeasurementState(hash, style.fontStyle.hashCode); + hash = _hashReusableIntrinsicMeasurementState(hash, style.position.hashCode); + hash = _hashReusableIntrinsicMeasurementState(hash, style.whiteSpace.hashCode); + hash = _hashReusableIntrinsicMeasurementState(hash, style.wordBreak.hashCode); + hash = _hashReusableIntrinsicMeasurementState(hash, style.textAlign.hashCode); + hash = _hashReusableIntrinsicMeasurementState(hash, style.fontStyle.hashCode); hash = _hashReusableIntrinsicMeasurementState(hash, style.fontWeight.value); hash = _hashReusableIntrinsicMeasurementState( hash, @@ -2819,26 +2478,18 @@ class RenderFlexLayout extends RenderLayoutBox { ); hash = _hashReusableIntrinsicMeasurementState( hash, - _quantizeReusableIntrinsicMeasurementDouble( - style.lineHeight.computedValue), + _quantizeReusableIntrinsicMeasurementDouble(style.lineHeight.computedValue), ); hash = _hashReusableIntrinsicMeasurementState( hash, - _quantizeReusableIntrinsicMeasurementDouble( - style.textIndent.computedValue), + _quantizeReusableIntrinsicMeasurementDouble(style.textIndent.computedValue), ); - hash = - _hashReusableIntrinsicMeasurementState(hash, style.width.type.hashCode); - hash = _hashReusableIntrinsicMeasurementState( - hash, style.height.type.hashCode); - hash = _hashReusableIntrinsicMeasurementState( - hash, style.minWidth.type.hashCode); - hash = _hashReusableIntrinsicMeasurementState( - hash, style.maxWidth.type.hashCode); - hash = _hashReusableIntrinsicMeasurementState( - hash, style.minHeight.type.hashCode); - hash = _hashReusableIntrinsicMeasurementState( - hash, style.maxHeight.type.hashCode); + hash = _hashReusableIntrinsicMeasurementState(hash, style.width.type.hashCode); + hash = _hashReusableIntrinsicMeasurementState(hash, style.height.type.hashCode); + hash = _hashReusableIntrinsicMeasurementState(hash, style.minWidth.type.hashCode); + hash = _hashReusableIntrinsicMeasurementState(hash, style.maxWidth.type.hashCode); + hash = _hashReusableIntrinsicMeasurementState(hash, style.minHeight.type.hashCode); + hash = _hashReusableIntrinsicMeasurementState(hash, style.maxHeight.type.hashCode); if (style.width.isNotAuto) { hash = _hashReusableIntrinsicMeasurementState( hash, @@ -2854,29 +2505,25 @@ class RenderFlexLayout extends RenderLayoutBox { if (style.minWidth.isNotAuto) { hash = _hashReusableIntrinsicMeasurementState( hash, - _quantizeReusableIntrinsicMeasurementDouble( - style.minWidth.computedValue), + _quantizeReusableIntrinsicMeasurementDouble(style.minWidth.computedValue), ); } if (!style.maxWidth.isNone) { hash = _hashReusableIntrinsicMeasurementState( hash, - _quantizeReusableIntrinsicMeasurementDouble( - style.maxWidth.computedValue), + _quantizeReusableIntrinsicMeasurementDouble(style.maxWidth.computedValue), ); } if (style.minHeight.isNotAuto) { hash = _hashReusableIntrinsicMeasurementState( hash, - _quantizeReusableIntrinsicMeasurementDouble( - style.minHeight.computedValue), + _quantizeReusableIntrinsicMeasurementDouble(style.minHeight.computedValue), ); } if (!style.maxHeight.isNone) { hash = _hashReusableIntrinsicMeasurementState( hash, - _quantizeReusableIntrinsicMeasurementDouble( - style.maxHeight.computedValue), + _quantizeReusableIntrinsicMeasurementDouble(style.maxHeight.computedValue), ); } if (style.flexBasis != null) { @@ -2885,8 +2532,7 @@ class RenderFlexLayout extends RenderLayoutBox { style.flexBasis!.type.hashCode, ); } - final InlineFormattingContext? ifc = - effectiveFlowChild.inlineFormattingContext; + final InlineFormattingContext? ifc = effectiveFlowChild.inlineFormattingContext; if (ifc != null) { hash = _hashReusableIntrinsicMeasurementState( hash, @@ -2898,9 +2544,11 @@ class RenderFlexLayout extends RenderLayoutBox { _FlexIntrinsicMeasurementLookupResult _lookupReusableIntrinsicMeasurement( RenderBox child, - BoxConstraints childConstraints, { + BoxConstraints childConstraints, + { bool allowAnonymous = false, - }) { + } + ) { if (!allowAnonymous) { return const _FlexIntrinsicMeasurementLookupResult(); } @@ -2911,8 +2559,7 @@ class RenderFlexLayout extends RenderLayoutBox { child, allowAnonymous: allowAnonymous, ); - if (!_isHorizontalFlexDirection || - renderStyle.flexWrap != FlexWrap.nowrap) { + if (!_isHorizontalFlexDirection || renderStyle.flexWrap != FlexWrap.nowrap) { return const _FlexIntrinsicMeasurementLookupResult(); } if (_hasBaselineAlignmentForChild(child)) { @@ -2987,13 +2634,12 @@ class RenderFlexLayout extends RenderLayoutBox { } final _FlexIntrinsicMeasurementCacheBucket bucket = _childrenIntrinsicMeasureCache[child] ?? - _FlexIntrinsicMeasurementCacheBucket(); + _FlexIntrinsicMeasurementCacheBucket(); bucket.store(_FlexIntrinsicMeasurementCacheEntry( constraints: childConstraints, size: Size.copy(childSize), intrinsicMainSize: intrinsicMainSize, - reusableStateSignature: - _computeReusableIntrinsicMeasurementStateSignature( + reusableStateSignature: _computeReusableIntrinsicMeasurementStateSignature( child, childConstraints, flowChild: flowChild, @@ -3010,8 +2656,7 @@ class RenderFlexLayout extends RenderLayoutBox { if (flowChild == null) { return false; } - return child is RenderEventListener || - flowChild.renderStyle.isSelfAnonymousFlowLayout(); + return child is RenderEventListener || flowChild.renderStyle.isSelfAnonymousFlowLayout(); } bool _shouldAvoidParentUsesSizeForFlexChild(RenderBox child) { @@ -3155,8 +2800,8 @@ class RenderFlexLayout extends RenderLayoutBox { } } - if (effectiveRoot is ContainerRenderObjectMixin>) { + if (effectiveRoot + is ContainerRenderObjectMixin>) { RenderBox? child = (effectiveRoot as dynamic).firstChild as RenderBox?; while (child != null) { if (_flowSubtreeContainsReusableTextHeavyContent(child)) { @@ -3363,8 +3008,7 @@ class RenderFlexLayout extends RenderLayoutBox { return true; } - List<_RunMetrics>? _tryBuildEarlyNoFlexNoStretchNoBaselineRunMetrics( - List children) { + List<_RunMetrics>? _tryBuildEarlyNoFlexNoStretchNoBaselineRunMetrics(List children) { if (!_isHorizontalFlexDirection) { _recordEarlyFastPathReject( _FlexFastPathRejectReason.verticalDirection, @@ -3414,8 +3058,7 @@ class RenderFlexLayout extends RenderLayoutBox { _layoutChildForFlex(child, childConstraints); _cacheOriginalConstraintsIfNeeded(child, childConstraints); - final RenderLayoutParentData? childParentData = - child.parentData as RenderLayoutParentData?; + final RenderLayoutParentData? childParentData = child.parentData as RenderLayoutParentData?; childParentData?.runIndex = 0; final double childMainSize = _getMainSize(child); @@ -3425,11 +3068,9 @@ class RenderFlexLayout extends RenderLayoutBox { runMainAxisExtent += mainAxisGap; } runMainAxisExtent += _getMainAxisExtent(child); - runCrossAxisExtent = - math.max(runCrossAxisExtent, _getCrossAxisExtent(child)); + runCrossAxisExtent = math.max(runCrossAxisExtent, _getCrossAxisExtent(child)); - final RenderBoxModel? effectiveChild = - child is RenderBoxModel ? child : null; + final RenderBoxModel? effectiveChild = child is RenderBoxModel ? child : null; final _RunChild runChild = _createRunChildMetadata( child, childMainSize, @@ -3447,8 +3088,7 @@ class RenderFlexLayout extends RenderLayoutBox { } final List<_RunMetrics> runMetrics = <_RunMetrics>[ - _RunMetrics(runMainAxisExtent, runCrossAxisExtent, totalFlexGrow, - totalFlexShrink, 0, runChildren, 0) + _RunMetrics(runMainAxisExtent, runCrossAxisExtent, totalFlexGrow, totalFlexShrink, 0, runChildren, 0) ]; _flexLineBoxMetrics = runMetrics; @@ -3493,14 +3133,11 @@ class RenderFlexLayout extends RenderLayoutBox { // Prepare children of different type for layout. RenderBox? child = firstChild; while (child != null) { - final RenderLayoutParentData childParentData = - child.parentData as RenderLayoutParentData; + final RenderLayoutParentData childParentData = child.parentData as RenderLayoutParentData; if (child is RenderBoxModel && - (child.renderStyle.isSelfPositioned() || - child.renderStyle.isSelfStickyPosition())) { + (child.renderStyle.isSelfPositioned() || child.renderStyle.isSelfStickyPosition())) { positionedChildren.add(child); - } else if (child is RenderPositionPlaceholder && - _isPlaceholderPositioned(child)) { + } else if (child is RenderPositionPlaceholder && _isPlaceholderPositioned(child)) { positionPlaceholderChildren.add(child); } else { flexItemChildren.add(child); @@ -3545,8 +3182,7 @@ class RenderFlexLayout extends RenderLayoutBox { for (RenderBoxModel child in positionedChildren) { final CSSRenderStyle rs = child.renderStyle; final CSSPositionType pos = rs.position; - final bool isAbsOrFixed = - pos == CSSPositionType.absolute || pos == CSSPositionType.fixed; + final bool isAbsOrFixed = pos == CSSPositionType.absolute || pos == CSSPositionType.fixed; final bool hasExplicitMaxHeight = !rs.maxHeight.isNone; final bool hasExplicitMinHeight = !rs.minHeight.isAuto; if (isAbsOrFixed && @@ -3556,8 +3192,7 @@ class RenderFlexLayout extends RenderLayoutBox { rs.bottom.isNotAuto && !hasExplicitMaxHeight && !hasExplicitMinHeight) { - CSSPositionedLayout.layoutPositionedChild(this, child, - needsRelayout: true); + CSSPositionedLayout.layoutPositionedChild(this, child, needsRelayout: true); } } @@ -3566,8 +3201,7 @@ class RenderFlexLayout extends RenderLayoutBox { } // init overflowLayout size - initOverflowLayout(Rect.fromLTRB(0, 0, size.width, size.height), - Rect.fromLTRB(0, 0, size.width, size.height)); + initOverflowLayout(Rect.fromLTRB(0, 0, size.width, size.height), Rect.fromLTRB(0, 0, size.width, size.height)); // calculate all flexItem child overflow size addOverflowLayoutFromChildren(orderedChildren); @@ -3619,11 +3253,9 @@ class RenderFlexLayout extends RenderLayoutBox { return; } - List<_RunMetrics>? runMetrics = - _tryBuildEarlyNoFlexNoStretchNoBaselineRunMetrics(children); + List<_RunMetrics>? runMetrics = _tryBuildEarlyNoFlexNoStretchNoBaselineRunMetrics(children); if (runMetrics != null) { - final bool hasStretchedChildren = - _hasStretchedChildrenInCrossAxis(runMetrics); + final bool hasStretchedChildren = _hasStretchedChildrenInCrossAxis(runMetrics); if (!hasStretchedChildren && _canAttemptFullEarlyFastPath(runMetrics)) { final _FlexResolutionInputs inputs = _computeFlexResolutionInputs(); if (_tryNoFlexNoStretchNoBaselineFastPath( @@ -3658,8 +3290,7 @@ class RenderFlexLayout extends RenderLayoutBox { if (runMetrics == null) { // Layout children to compute metrics of flex lines. if (!kReleaseMode) { - developer.Timeline.startSync( - 'RenderFlex.layoutFlexItems.computeRunMetrics', + developer.Timeline.startSync('RenderFlex.layoutFlexItems.computeRunMetrics', arguments: {'renderObject': describeIdentity(this)}); } @@ -3673,8 +3304,7 @@ class RenderFlexLayout extends RenderLayoutBox { _setContainerSize(runMetrics); if (!kReleaseMode) { - developer.Timeline.startSync( - 'RenderFlex.layoutFlexItems.adjustChildrenSize'); + developer.Timeline.startSync('RenderFlex.layoutFlexItems.adjustChildrenSize'); } // Adjust children size based on flex properties which may affect children size. @@ -3688,8 +3318,7 @@ class RenderFlexLayout extends RenderLayoutBox { } if (!kReleaseMode) { - developer.Timeline.startSync( - 'RenderFlex.layoutFlexItems.setChildrenOffset'); + developer.Timeline.startSync('RenderFlex.layoutFlexItems.setChildrenOffset'); } // Set children offset based on flex alignment properties. @@ -3700,8 +3329,7 @@ class RenderFlexLayout extends RenderLayoutBox { } if (!kReleaseMode) { - developer.Timeline.startSync( - 'RenderFlex.layoutFlexItems.setMaxScrollableSize'); + developer.Timeline.startSync('RenderFlex.layoutFlexItems.setMaxScrollableSize'); } // Set the size of scrollable overflow area for flex layout. @@ -3720,32 +3348,27 @@ class RenderFlexLayout extends RenderLayoutBox { // Cache CSS baselines for this flex container during layout to avoid cross-child baseline computation later. double? containerBaseline; CSSDisplay? effectiveDisplay = renderStyle.effectiveDisplay; - bool isDisplayInline = effectiveDisplay != CSSDisplay.block && - effectiveDisplay != CSSDisplay.flex; + bool isDisplayInline = effectiveDisplay != CSSDisplay.block && effectiveDisplay != CSSDisplay.flex; double? getChildBaselineDistance(RenderBox child) { // Prefer WebF's cached CSS baselines, which are safe to access even when the // render tree is not attached to a PipelineOwner (e.g. offscreen/manual layout). if (child is RenderBoxModel) { - final double? css = - child.computeCssFirstBaselineOf(TextBaseline.alphabetic); + final double? css = child.computeCssFirstBaselineOf(TextBaseline.alphabetic); if (css != null) return css; } else if (child is RenderPositionPlaceholder) { final RenderBoxModel? positioned = child.positioned; - final double? css = - positioned?.computeCssFirstBaselineOf(TextBaseline.alphabetic); + final double? css = positioned?.computeCssFirstBaselineOf(TextBaseline.alphabetic); if (css != null) return css; } // Avoid RenderBox.getDistanceToBaseline when detached; it asserts on owner!. if (!child.attached) { if (child is RenderBoxModel) { - return child.boxSize?.height ?? - (child.hasSize ? child.size.height : null); + return child.boxSize?.height ?? (child.hasSize ? child.size.height : null); } if (child is RenderPositionPlaceholder) { - return child.boxSize?.height ?? - (child.hasSize ? child.size.height : null); + return child.boxSize?.height ?? (child.hasSize ? child.size.height : null); } return child.hasSize ? child.size.height : null; } @@ -3783,11 +3406,9 @@ class RenderFlexLayout extends RenderLayoutBox { // baseline-aligned item no longer anchors the container baseline in IFC. if (_isChildCrossAxisMarginAutoExist(candidate)) return false; final AlignSelf self = _getAlignSelf(candidate); - if (self == AlignSelf.baseline || self == AlignSelf.lastBaseline) - return true; + if (self == AlignSelf.baseline || self == AlignSelf.lastBaseline) return true; if (self == AlignSelf.auto && - (renderStyle.alignItems == AlignItems.baseline || - renderStyle.alignItems == AlignItems.lastBaseline)) { + (renderStyle.alignItems == AlignItems.baseline || renderStyle.alignItems == AlignItems.lastBaseline)) { return true; } return false; @@ -3831,8 +3452,7 @@ class RenderFlexLayout extends RenderLayoutBox { // For inline flex containers, include bottom margin to synthesize an // external baseline from the bottom margin edge. if (isDisplayInline) { - containerBaseline = - borderBoxHeight + renderStyle.marginBottom.computedValue; + containerBaseline = borderBoxHeight + renderStyle.marginBottom.computedValue; } else { containerBaseline = borderBoxHeight; } @@ -3850,18 +3470,15 @@ class RenderFlexLayout extends RenderLayoutBox { final List<_RunChild> firstRunChildren = firstLineMetrics.runChildren; if (firstRunChildren.isNotEmpty) { RenderBox? baselineChild; - double? - baselineDistance; // distance from child's border-top to its baseline + double? baselineDistance; // distance from child's border-top to its baseline RenderBox? fallbackChild; double? fallbackBaseline; bool participatesInBaseline(RenderBox candidate) { final AlignSelf self = _getAlignSelf(candidate); - if (self == AlignSelf.baseline || self == AlignSelf.lastBaseline) - return true; + if (self == AlignSelf.baseline || self == AlignSelf.lastBaseline) return true; if (self == AlignSelf.auto && - (renderStyle.alignItems == AlignItems.baseline || - renderStyle.alignItems == AlignItems.lastBaseline)) { + (renderStyle.alignItems == AlignItems.baseline || renderStyle.alignItems == AlignItems.lastBaseline)) { return true; } return false; @@ -3895,8 +3512,7 @@ class RenderFlexLayout extends RenderLayoutBox { (self == AlignSelf.baseline || self == AlignSelf.lastBaseline || (self == AlignSelf.auto && - (renderStyle.alignItems == AlignItems.baseline || - renderStyle.alignItems == AlignItems.lastBaseline))); + (renderStyle.alignItems == AlignItems.baseline || renderStyle.alignItems == AlignItems.lastBaseline))); double dy = 0; if (child.parentData is RenderLayoutParentData) { dy = (child.parentData as RenderLayoutParentData).offset.dy; @@ -3921,8 +3537,7 @@ class RenderFlexLayout extends RenderLayoutBox { // Prefer the first baseline-participating child (excluding cross-axis auto margins); // otherwise fall back to the first child that exposes a baseline; otherwise the first item. final RenderBox? chosen = baselineChild ?? fallbackChild; - final double? chosenBaseline = - baselineChild != null ? baselineDistance : fallbackBaseline; + final double? chosenBaseline = baselineChild != null ? baselineDistance : fallbackBaseline; if (chosen != null) { if (chosenBaseline != null) { @@ -3934,8 +3549,7 @@ class RenderFlexLayout extends RenderLayoutBox { dy = pd.offset.dy; } if (chosen is RenderBoxModel) { - final Offset? rel = - CSSPositionedLayout.getRelativeOffset(chosen.renderStyle); + final Offset? rel = CSSPositionedLayout.getRelativeOffset(chosen.renderStyle); if (rel != null) dy -= rel.dy; } containerBaseline = chosenBaseline + dy; @@ -3944,8 +3558,7 @@ class RenderFlexLayout extends RenderLayoutBox { final double borderBoxHeight = boxSize?.height ?? size.height; // If inline-level (inline-flex), synthesize from bottom margin edge. if (isDisplayInline) { - containerBaseline = - borderBoxHeight + renderStyle.marginBottom.computedValue; + containerBaseline = borderBoxHeight + renderStyle.marginBottom.computedValue; } else { containerBaseline = borderBoxHeight; } @@ -3961,8 +3574,7 @@ class RenderFlexLayout extends RenderLayoutBox { List positionPlaceholderChildren = [child]; // Layout children to compute metrics of flex lines. - List<_RunMetrics> runMetrics = - _computeRunMetrics(positionPlaceholderChildren); + List<_RunMetrics> runMetrics = _computeRunMetrics(positionPlaceholderChildren); // Set children offset based on flex alignment properties. _setChildrenOffset(runMetrics); @@ -3970,15 +3582,12 @@ class RenderFlexLayout extends RenderLayoutBox { // Layout children in normal flow order to calculate metrics of flex lines according to its constraints // and flex-wrap property. - List<_RunMetrics> _computeRunMetrics( - List children, - ) { + List<_RunMetrics> _computeRunMetrics(List children,) { List<_RunMetrics> runMetrics = <_RunMetrics>[]; if (children.isEmpty) return runMetrics; final bool isHorizontal = _isHorizontalFlexDirection; - final bool isWrap = renderStyle.flexWrap == FlexWrap.wrap || - renderStyle.flexWrap == FlexWrap.wrapReverse; + final bool isWrap = renderStyle.flexWrap == FlexWrap.wrap || renderStyle.flexWrap == FlexWrap.wrapReverse; final double mainAxisGap = _getMainAxisGap(); double runMainAxisExtent = 0.0; @@ -4015,8 +3624,9 @@ class RenderFlexLayout extends RenderLayoutBox { for (int childIndex = 0; childIndex < children.length; childIndex++) { final RenderBox child = children[childIndex]; final BoxConstraints childConstraints = _getIntrinsicConstraints(child); - final bool isMetricsOnlyMeasureChild = allowAnonymousMetricsOnlyCache && - _isMetricsOnlyIntrinsicMeasureChild(child); + final bool isMetricsOnlyMeasureChild = + allowAnonymousMetricsOnlyCache && + _isMetricsOnlyIntrinsicMeasureChild(child); final _FlexIntrinsicMeasurementLookupResult cacheLookup = _lookupReusableIntrinsicMeasurement( child, @@ -4078,27 +3688,24 @@ class RenderFlexLayout extends RenderLayoutBox { double? candidate; if (flowChild.inlineFormattingContext != null) { // Paragraph max-intrinsic width approximates the max-content contribution. - final double paraMax = flowChild - .inlineFormattingContext!.paragraphMaxIntrinsicWidth; + final double paraMax = flowChild.inlineFormattingContext!.paragraphMaxIntrinsicWidth; // Convert content-width to border-box width by adding horizontal padding + borders. - final double paddingBorderH = cs.paddingLeft.computedValue + - cs.paddingRight.computedValue + - cs.effectiveBorderLeftWidth.computedValue + - cs.effectiveBorderRightWidth.computedValue; + final double paddingBorderH = + cs.paddingLeft.computedValue + + cs.paddingRight.computedValue + + cs.effectiveBorderLeftWidth.computedValue + + cs.effectiveBorderRightWidth.computedValue; candidate = (paraMax.isFinite ? paraMax : 0) + paddingBorderH; } else { // Fallback: use max intrinsic width (already includes padding/border). - final double maxIntrinsic = - flowChild.getMaxIntrinsicWidth(double.infinity); + final double maxIntrinsic = flowChild.getMaxIntrinsicWidth(double.infinity); if (maxIntrinsic.isFinite) { candidate = maxIntrinsic; } } // If the currently measured intrinsic width is larger (e.g., filled to container), // prefer the content-based candidate to avoid unintended expansion. - if (candidate != null && - candidate > 0 && - candidate < intrinsicMain) { + if (candidate != null && candidate > 0 && candidate < intrinsicMain) { intrinsicMain = candidate; } } @@ -4122,14 +3729,10 @@ class RenderFlexLayout extends RenderLayoutBox { // intrinsicMain is the border-box main size. In WebF (border-box model), // min-width/max-width are already specified for the border box. Do not // add padding/border again when clamping. - if (maxMain != null && - maxMain.isFinite && - intrinsicMain > maxMain) { + if (maxMain != null && maxMain.isFinite && intrinsicMain > maxMain) { intrinsicMain = maxMain; } - if (minMain != null && - minMain.isFinite && - intrinsicMain < minMain) { + if (minMain != null && minMain.isFinite && intrinsicMain < minMain) { intrinsicMain = minMain; } } @@ -4140,21 +3743,18 @@ class RenderFlexLayout extends RenderLayoutBox { bool hasPctMaxMain = isHorizontal ? child.renderStyle.maxWidth.type == CSSLengthType.PERCENTAGE : child.renderStyle.maxHeight.type == CSSLengthType.PERCENTAGE; - bool hasAutoMain = isHorizontal - ? child.renderStyle.width.isAuto - : child.renderStyle.height.isAuto; + bool hasAutoMain = isHorizontal ? child.renderStyle.width.isAuto : child.renderStyle.height + .isAuto; if (hasPctMaxMain && hasAutoMain) { double paddingBorderMain = isHorizontal ? (child.renderStyle.effectiveBorderLeftWidth.computedValue + - child - .renderStyle.effectiveBorderRightWidth.computedValue + - child.renderStyle.paddingLeft.computedValue + - child.renderStyle.paddingRight.computedValue) + child.renderStyle.effectiveBorderRightWidth.computedValue + + child.renderStyle.paddingLeft.computedValue + + child.renderStyle.paddingRight.computedValue) : (child.renderStyle.effectiveBorderTopWidth.computedValue + - child.renderStyle.effectiveBorderBottomWidth - .computedValue + - child.renderStyle.paddingTop.computedValue + - child.renderStyle.paddingBottom.computedValue); + child.renderStyle.effectiveBorderBottomWidth.computedValue + + child.renderStyle.paddingTop.computedValue + + child.renderStyle.paddingBottom.computedValue); // Check if this is an empty element (no content) using DOM-based detection bool isEmptyElement = false; @@ -4177,28 +3777,24 @@ class RenderFlexLayout extends RenderLayoutBox { } } - _storeIntrinsicMeasurementCache( - child, childConstraints, childSize, intrinsicMain); + _storeIntrinsicMeasurementCache(child, childConstraints, childSize, intrinsicMain); } - final RenderLayoutParentData? childParentData = - child.parentData as RenderLayoutParentData?; + final RenderLayoutParentData? childParentData = child.parentData as RenderLayoutParentData?; _childrenIntrinsicMainSizes[child] = intrinsicMain; - Size? intrinsicChildSize = - _getChildSize(child, shouldUseIntrinsicMainSize: true); + Size? intrinsicChildSize = _getChildSize(child, shouldUseIntrinsicMainSize: true); - double childMainAxisExtent = - _getMainAxisExtent(child, shouldUseIntrinsicMainSize: true); + double childMainAxisExtent = _getMainAxisExtent(child, shouldUseIntrinsicMainSize: true); double childCrossAxisExtent = _getCrossAxisExtent(child); // Include gap spacing in flex line limit check double gapSpacing = runChildren.isNotEmpty ? mainAxisGap : 0; - bool isExceedFlexLineLimit = - runMainAxisExtent + gapSpacing + childMainAxisExtent > - flexLineLimit; + bool isExceedFlexLineLimit = runMainAxisExtent + gapSpacing + childMainAxisExtent > flexLineLimit; // calculate flex line - if (isWrap && runChildren.isNotEmpty && isExceedFlexLineLimit) { + if (isWrap && + runChildren.isNotEmpty && + isExceedFlexLineLimit) { runMetrics.add(_RunMetrics( runMainAxisExtent, runCrossAxisExtent, @@ -4226,7 +3822,8 @@ class RenderFlexLayout extends RenderLayoutBox { // Vertical align is only valid for inline box. // Baseline alignment in column direction behave the same as flex-start. AlignSelf alignSelf = _getAlignSelf(child); - bool isBaselineAlign = alignSelf == AlignSelf.baseline || + bool isBaselineAlign = + alignSelf == AlignSelf.baseline || alignSelf == AlignSelf.lastBaseline || renderStyle.alignItems == AlignItems.baseline || renderStyle.alignItems == AlignItems.lastBaseline; @@ -4252,10 +3849,7 @@ class RenderFlexLayout extends RenderLayoutBox { maxSizeAboveBaseline, ); maxSizeBelowBaseline = math.max( - childMarginTop + - childMarginBottom + - intrinsicChildSize!.height - - childAscent, + childMarginTop + childMarginBottom + intrinsicChildSize!.height - childAscent, maxSizeBelowBaseline, ); runCrossAxisExtent = maxSizeAboveBaseline + maxSizeBelowBaseline; @@ -4266,8 +3860,7 @@ class RenderFlexLayout extends RenderLayoutBox { 'runCross=${runCrossAxisExtent.toStringAsFixed(2)}'); } } else { - runCrossAxisExtent = - math.max(runCrossAxisExtent, childCrossAxisExtent); + runCrossAxisExtent = math.max(runCrossAxisExtent, childCrossAxisExtent); } // Per CSS Flexbox §9.7, keep two sizes: @@ -4277,10 +3870,8 @@ class RenderFlexLayout extends RenderLayoutBox { // We store the base size in runChild.originalMainSize so remaining free // space and shrink/grow weighting use the correct base, and keep the // clamped value in _childrenIntrinsicMainSizes for line metrics. - final RenderBoxModel? effectiveChild = - child is RenderBoxModel ? child : null; - final double? usedFlexBasis = - effectiveChild != null ? _getUsedFlexBasis(child) : null; + final RenderBoxModel? effectiveChild = child is RenderBoxModel ? child : null; + final double? usedFlexBasis = effectiveChild != null ? _getUsedFlexBasis(child) : null; double baseMainSize; if (usedFlexBasis != null) { @@ -4289,9 +3880,7 @@ class RenderFlexLayout extends RenderLayoutBox { // items share free space evenly regardless of padding/border, while the // non-flex portion (padding/border) is accounted for separately in totalSpace. final CSSLengthValue? fb = effectiveChild?.renderStyle.flexBasis; - if (fb != null && - fb.type == CSSLengthType.PERCENTAGE && - fb.computedValue == 0) { + if (fb != null && fb.type == CSSLengthType.PERCENTAGE && fb.computedValue == 0) { baseMainSize = 0; } else { baseMainSize = usedFlexBasis; @@ -4348,14 +3937,10 @@ class RenderFlexLayout extends RenderLayoutBox { for (RenderBox child in children) { RenderBoxModel? box = child is RenderBoxModel ? child - : (child is RenderEventListener - ? child.child as RenderBoxModel? - : null); + : (child is RenderEventListener ? child.child as RenderBoxModel? : null); if (box != null) { - bool hasPercentageMaxWidth = - box.renderStyle.maxWidth.type == CSSLengthType.PERCENTAGE; - bool hasPercentageMaxHeight = - box.renderStyle.maxHeight.type == CSSLengthType.PERCENTAGE; + bool hasPercentageMaxWidth = box.renderStyle.maxWidth.type == CSSLengthType.PERCENTAGE; + bool hasPercentageMaxHeight = box.renderStyle.maxHeight.type == CSSLengthType.PERCENTAGE; if (hasPercentageMaxWidth || hasPercentageMaxHeight) { // Store the final constraints for use in _adjustChildrenSize @@ -4369,9 +3954,7 @@ class RenderFlexLayout extends RenderLayoutBox { } // Compute the leading and between spacing of each flex line. - _RunSpacing _computeRunSpacing( - List<_RunMetrics> runMetrics, - ) { + _RunSpacing _computeRunSpacing(List<_RunMetrics> runMetrics,) { double? contentBoxLogicalWidth = renderStyle.contentBoxLogicalWidth; double? contentBoxLogicalHeight = renderStyle.contentBoxLogicalHeight; double containerCrossAxisExtent = 0.0; @@ -4391,8 +3974,7 @@ class RenderFlexLayout extends RenderLayoutBox { double runBetweenSpace = 0.0; // Align-content only works in when flex-wrap is no nowrap. - if (renderStyle.flexWrap == FlexWrap.wrap || - renderStyle.flexWrap == FlexWrap.wrapReverse) { + if (renderStyle.flexWrap == FlexWrap.wrap || renderStyle.flexWrap == FlexWrap.wrapReverse) { switch (renderStyle.alignContent) { case AlignContent.flexStart: case AlignContent.start: @@ -4408,8 +3990,7 @@ class RenderFlexLayout extends RenderLayoutBox { if (crossAxisFreeSpace < 0) { runBetweenSpace = 0; } else { - runBetweenSpace = - runCount > 1 ? crossAxisFreeSpace / (runCount - 1) : 0.0; + runBetweenSpace = runCount > 1 ? crossAxisFreeSpace / (runCount - 1) : 0.0; } break; case AlignContent.spaceAround: @@ -4451,9 +4032,7 @@ class RenderFlexLayout extends RenderLayoutBox { // Find the size in the cross axis of flex lines. // @TODO: add cache to avoid recalculate in one layout stage. - double _getRunsCrossSize( - List<_RunMetrics> runMetrics, - ) { + double _getRunsCrossSize(List<_RunMetrics> runMetrics,) { double crossSize = 0; double crossAxisGap = _getCrossAxisGap(); for (int i = 0; i < runMetrics.length; i++) { @@ -4468,12 +4047,9 @@ class RenderFlexLayout extends RenderLayoutBox { // Find the max size in the main axis of flex lines. // @TODO: add cache to avoid recalculate in one layout stage. - double _getRunsMaxMainSize( - List<_RunMetrics> runMetrics, - ) { + double _getRunsMaxMainSize(List<_RunMetrics> runMetrics,) { // Find the max size of flex lines. - _RunMetrics maxMainSizeMetrics = - runMetrics.reduce((_RunMetrics curr, _RunMetrics next) { + _RunMetrics maxMainSizeMetrics = runMetrics.reduce((_RunMetrics curr, _RunMetrics next) { return curr.mainAxisExtent > next.mainAxisExtent ? curr : next; }); return maxMainSizeMetrics.mainAxisExtent; @@ -4481,11 +4057,7 @@ class RenderFlexLayout extends RenderLayoutBox { // Resolve flex item length if flex-grow or flex-shrink exists. // https://www.w3.org/TR/css-flexbox-1/#resolve-flexible-lengths - bool _resolveFlexibleLengths( - _RunMetrics runMetric, - _FlexFactorTotals totalFlexFactor, - double initialFreeSpace, - ) { + bool _resolveFlexibleLengths(_RunMetrics runMetric, _FlexFactorTotals totalFlexFactor, double initialFreeSpace,) { final List<_RunChild> runChildren = runMetric.runChildren; final double totalFlexGrow = totalFlexFactor.flexGrow; final double totalFlexShrink = totalFlexFactor.flexShrink; @@ -4494,8 +4066,7 @@ class RenderFlexLayout extends RenderLayoutBox { bool isFlexGrow = initialFreeSpace > 0 && totalFlexGrow > 0; bool isFlexShrink = initialFreeSpace < 0 && totalFlexShrink > 0; - double sumFlexFactors = - isFlexGrow ? totalFlexGrow : (isFlexShrink ? totalFlexShrink : 0); + double sumFlexFactors = isFlexGrow ? totalFlexGrow : (isFlexShrink ? totalFlexShrink : 0); // Per CSS Flexbox §9.7, if the sum of the unfrozen flex items’ flex // factors is less than 1, multiply the free space by this sum. @@ -4528,8 +4099,7 @@ class RenderFlexLayout extends RenderLayoutBox { final double childFlexShrink = runChild.flexShrink; if (childFlexShrink == 0) continue; - final double baseSize = - (runChild.usedFlexBasis ?? runChild.originalMainSize); + final double baseSize = (runChild.usedFlexBasis ?? runChild.originalMainSize); totalWeightedFlexShrink += baseSize * childFlexShrink; } } @@ -4539,8 +4109,7 @@ class RenderFlexLayout extends RenderLayoutBox { if (runChild.frozen) continue; // Use used flex-basis (border-box) for originalMainSize when definite. - final double originalMainSize = - runChild.usedFlexBasis ?? runChild.originalMainSize; + final double originalMainSize = runChild.usedFlexBasis ?? runChild.originalMainSize; double computedSize = originalMainSize; double flexedMainSize = originalMainSize; @@ -4550,8 +4119,7 @@ class RenderFlexLayout extends RenderLayoutBox { // Re-evaluate grow/shrink based on current remaining free space sign. final bool doGrow = spacePerFlex != null && flexGrow > 0; - final bool doShrink = - remainingFreeSpace < 0 && totalFlexShrink > 0 && flexShrink > 0; + final bool doShrink = remainingFreeSpace < 0 && totalFlexShrink > 0 && flexShrink > 0; if (doGrow) { computedSize = originalMainSize + spacePerFlex * flexGrow; @@ -4562,8 +4130,7 @@ class RenderFlexLayout extends RenderLayoutBox { // distribution itself. if (totalWeightedFlexShrink != 0) { final double scaledShrink = originalMainSize * flexShrink; - computedSize = originalMainSize + - (scaledShrink / totalWeightedFlexShrink) * remainingFreeSpace; + computedSize = originalMainSize + (scaledShrink / totalWeightedFlexShrink) * remainingFreeSpace; } } @@ -4583,20 +4150,15 @@ class RenderFlexLayout extends RenderLayoutBox { if (clampTarget != null) { double minMainAxisSize = _getMinMainAxisSize(clampTarget); double maxMainAxisSize = _getMaxMainAxisSize(clampTarget); - if (computedSize < minMainAxisSize && - (computedSize - minMainAxisSize).abs() >= minFlexPrecision) { + if (computedSize < minMainAxisSize && (computedSize - minMainAxisSize).abs() >= minFlexPrecision) { flexedMainSize = minMainAxisSize; - } else if (computedSize > maxMainAxisSize && - (computedSize - maxMainAxisSize).abs() >= minFlexPrecision) { + } else if (computedSize > maxMainAxisSize && (computedSize - maxMainAxisSize).abs() >= minFlexPrecision) { flexedMainSize = maxMainAxisSize; } } } - double violation = - (flexedMainSize - computedSize).abs() >= minFlexPrecision - ? flexedMainSize - computedSize - : 0; + double violation = (flexedMainSize - computedSize).abs() >= minFlexPrecision ? flexedMainSize - computedSize : 0; // Collect all the flex items with violations. if (violation > 0) { @@ -4615,8 +4177,7 @@ class RenderFlexLayout extends RenderLayoutBox { runChild.frozen = true; } } else { - List<_RunChild> violations = - totalViolation < 0 ? maxViolations : minViolations; + List<_RunChild> violations = totalViolation < 0 ? maxViolations : minViolations; // Find all the violations, set main size and freeze all the flex items. for (int i = 0; i < violations.length; i++) { @@ -4626,8 +4187,7 @@ class RenderFlexLayout extends RenderLayoutBox { // item in this iteration (relative to its original size). If the // item was clamped to its min/max, flexedMainSize equals the clamp // result; its delta reflects how much free space it actually took. - runMetric.remainingFreeSpace -= - runChild.flexedMainSize - runChild.originalMainSize; + runMetric.remainingFreeSpace -= runChild.flexedMainSize - runChild.originalMainSize; double flexGrow = runChild.flexGrow; double flexShrink = runChild.flexShrink; @@ -4680,19 +4240,10 @@ class RenderFlexLayout extends RenderLayoutBox { _FlexFastPathRejectCallback? onReject, }) { final bool isHorizontal = _isHorizontalFlexDirection; - final bool adjustProfilerEnabled = _FlexAdjustFastPathProfiler.enabled; - final String? adjustProfilerPath = - adjustProfilerEnabled ? _describeFastPathContainer() : null; final double mainAxisGap = _getMainAxisGap(); final double containerStyleMin = isHorizontal - ? (renderStyle.minWidth.isNotAuto - ? renderStyle.minWidth.computedValue - : 0.0) - : (renderStyle.minHeight.isNotAuto - ? renderStyle.minHeight.computedValue - : 0.0); - int relayoutRowCount = 0; - int relayoutChildCount = 0; + ? (renderStyle.minWidth.isNotAuto ? renderStyle.minWidth.computedValue : 0.0) + : (renderStyle.minHeight.isNotAuto ? renderStyle.minHeight.computedValue : 0.0); // First, verify no run will actually enter flexible length resolution. for (final _RunMetrics metrics in runMetrics) { @@ -4700,8 +4251,7 @@ class RenderFlexLayout extends RenderLayoutBox { double totalSpace = 0; for (final _RunChild runChild in runChildrenList) { - final double childSpace = - runChild.usedFlexBasis ?? runChild.originalMainSize; + final double childSpace = runChild.usedFlexBasis ?? runChild.originalMainSize; totalSpace += childSpace + runChild.mainAxisMargin; } @@ -4710,8 +4260,7 @@ class RenderFlexLayout extends RenderLayoutBox { totalSpace += (itemCount - 1) * mainAxisGap; } - final double freeSpace = - maxMainSize != null ? (maxMainSize - totalSpace) : 0.0; + final double freeSpace = maxMainSize != null ? (maxMainSize - totalSpace) : 0.0; final bool boundedOnly = maxMainSize != null && !(isHorizontal @@ -4722,8 +4271,7 @@ class RenderFlexLayout extends RenderLayoutBox { (contentConstraints?.hasTightHeight ?? false) || constraints.hasTightHeight)); - final bool willShrink = - maxMainSize != null && freeSpace < 0 && metrics.totalFlexShrink > 0; + final bool willShrink = maxMainSize != null && freeSpace < 0 && metrics.totalFlexShrink > 0; bool willGrow = false; if (metrics.totalFlexGrow > 0 && !boundedOnly) { @@ -4737,18 +4285,6 @@ class RenderFlexLayout extends RenderLayoutBox { } if (willShrink) { - if (adjustProfilerEnabled && adjustProfilerPath != null) { - _FlexAdjustFastPathProfiler.recordReject( - adjustProfilerPath, - _FlexAdjustFastPathRejectReason.wouldShrink, - details: { - 'freeSpace': freeSpace.toStringAsFixed(2), - 'totalSpace': totalSpace.toStringAsFixed(2), - 'maxMainSize': maxMainSize?.toStringAsFixed(2), - 'totalFlexShrink': metrics.totalFlexShrink.toStringAsFixed(2), - }, - ); - } onReject?.call( _FlexFastPathRejectReason.wouldShrink, details: { @@ -4761,21 +4297,6 @@ class RenderFlexLayout extends RenderLayoutBox { return false; } if (willGrow) { - if (adjustProfilerEnabled && adjustProfilerPath != null) { - _FlexAdjustFastPathProfiler.recordReject( - adjustProfilerPath, - _FlexAdjustFastPathRejectReason.wouldGrow, - details: { - 'freeSpace': freeSpace.toStringAsFixed(2), - 'totalSpace': totalSpace.toStringAsFixed(2), - 'maxMainSize': maxMainSize?.toStringAsFixed(2), - 'totalFlexGrow': metrics.totalFlexGrow.toStringAsFixed(2), - 'boundedOnly': boundedOnly, - 'isMainSizeDefinite': isMainSizeDefinite, - 'containerStyleMin': containerStyleMin.toStringAsFixed(2), - }, - ); - } onReject?.call( _FlexFastPathRejectReason.wouldGrow, details: { @@ -4806,38 +4327,20 @@ class RenderFlexLayout extends RenderLayoutBox { final double childOldMainSize = _getMainSize(child); final double? desiredPreservedMain = _childrenIntrinsicMainSizes[child]; - _FlexAdjustFastPathRelayoutReason? relayoutReason; - bool needsLayout = false; - if (effectiveChild.needsRelayout) { - needsLayout = true; - relayoutReason = - _FlexAdjustFastPathRelayoutReason.effectiveChildNeedsRelayout; - } else if (_childrenRequirePostMeasureLayout[child] == true) { - needsLayout = true; - relayoutReason = _FlexAdjustFastPathRelayoutReason.postMeasureLayout; - } else if (desiredPreservedMain != null && - desiredPreservedMain != childOldMainSize) { + bool needsLayout = effectiveChild.needsRelayout || + (_childrenRequirePostMeasureLayout[child] == true); + if (!needsLayout && desiredPreservedMain != null && desiredPreservedMain != childOldMainSize) { needsLayout = true; - relayoutReason = - _FlexAdjustFastPathRelayoutReason.preservedMainMismatch; } if (!needsLayout && desiredPreservedMain != null) { final BoxConstraints applied = child.constraints; final bool autoMain = isHorizontal ? effectiveChild.renderStyle.width.isAuto : effectiveChild.renderStyle.height.isAuto; - final bool wasNonTightMain = - isHorizontal ? !applied.hasTightWidth : !applied.hasTightHeight; + final bool wasNonTightMain = isHorizontal ? !applied.hasTightWidth : !applied.hasTightHeight; if (autoMain && wasNonTightMain) { - final bool preservedMainMatches = - (desiredPreservedMain - childOldMainSize).abs() < 0.5; - final bool textOnlySubtree = _hasOnlyTextFlexRelayoutSubtree(child); - if (!preservedMainMatches || !textOnlySubtree) { - needsLayout = true; - relayoutReason = _FlexAdjustFastPathRelayoutReason - .autoMainWithNonTightConstraint; - } + needsLayout = true; } } @@ -4848,38 +4351,11 @@ class RenderFlexLayout extends RenderLayoutBox { final double measuredBorderW = _getChildSize(effectiveChild)!.width; if (measuredBorderW > availCross + 0.5) { needsLayout = true; - relayoutReason = - _FlexAdjustFastPathRelayoutReason.columnAutoCrossOverflow; } } } if (!needsLayout) continue; - relayoutReason ??= - _FlexAdjustFastPathRelayoutReason.effectiveChildNeedsRelayout; - if (adjustProfilerEnabled && adjustProfilerPath != null) { - final Map details = { - 'oldMain': childOldMainSize.toStringAsFixed(2), - 'desiredMain': desiredPreservedMain?.toStringAsFixed(2), - 'autoMain': isHorizontal - ? effectiveChild.renderStyle.width.isAuto - : effectiveChild.renderStyle.height.isAuto, - 'appliedMainTight': isHorizontal - ? child.constraints.hasTightWidth - : child.constraints.hasTightHeight, - }; - if (!isHorizontal && availCross.isFinite) { - details['availCross'] = availCross.toStringAsFixed(2); - } - _FlexAdjustFastPathProfiler.recordRelayout( - adjustProfilerPath, - _describeFastPathChild(child), - relayoutReason, - childConstraints: child.constraints, - details: details, - ); - } - relayoutChildCount++; _markFlexRelayoutForTextOnly(effectiveChild); @@ -4895,7 +4371,6 @@ class RenderFlexLayout extends RenderLayoutBox { } if (!didRelayout) continue; - relayoutRowCount++; // Recompute run extents from the final child sizes. double mainAxisExtent = 0; @@ -4910,30 +4385,20 @@ class RenderFlexLayout extends RenderLayoutBox { metrics.crossAxisExtent = crossAxisExtent; } - if (adjustProfilerEnabled && adjustProfilerPath != null) { - _FlexAdjustFastPathProfiler.recordHit( - adjustProfilerPath, - relayoutRowCount: relayoutRowCount, - relayoutChildCount: relayoutChildCount, - ); - } return true; } // Adjust children size (not include position placeholder) based on // flex factors (flex-grow/flex-shrink) and alignment in cross axis (align-items). // https://www.w3.org/TR/css-flexbox-1/#resolve-flexible-lengths - void _adjustChildrenSize( - List<_RunMetrics> runMetrics, - ) { + void _adjustChildrenSize(List<_RunMetrics> runMetrics,) { if (runMetrics.isEmpty) return; final bool isHorizontal = _isHorizontalFlexDirection; final double mainAxisGap = _getMainAxisGap(); final bool hasBaselineAlignment = _hasBaselineAlignedChildren(runMetrics); final bool canAttemptFastPath = isHorizontal && !hasBaselineAlignment; - final bool hasStretchedChildren = canAttemptFastPath - ? _hasStretchedChildrenInCrossAxis(runMetrics) - : true; + final bool hasStretchedChildren = + canAttemptFastPath ? _hasStretchedChildrenInCrossAxis(runMetrics) : true; final _FlexResolutionInputs inputs = _computeFlexResolutionInputs(); final double? contentBoxLogicalWidth = inputs.contentBoxLogicalWidth; final double? contentBoxLogicalHeight = inputs.contentBoxLogicalHeight; @@ -4965,8 +4430,7 @@ class RenderFlexLayout extends RenderLayoutBox { double totalSpace = 0; // Flex factor calculation depends on flex-basis if exists. for (final _RunChild runChild in runChildrenList) { - final double childSpace = - runChild.usedFlexBasis ?? runChild.originalMainSize; + final double childSpace = runChild.usedFlexBasis ?? runChild.originalMainSize; totalSpace += childSpace + runChild.mainAxisMargin; } @@ -4981,14 +4445,9 @@ class RenderFlexLayout extends RenderLayoutBox { // For definite main sizes (tight or specified) or auto main size bounded by a max constraint, // distribute free space per spec. Positive free space allows grow when flex-basis is 0 (e.g., flex: 1), // negative free space triggers shrink when items overflow. - final bool boundedOnly = maxMainSize != null && - !(isHorizontal - ? (contentBoxLogicalWidth != null || - (contentConstraints?.hasTightWidth ?? false) || - constraints.hasTightWidth) - : (contentBoxLogicalHeight != null || - (contentConstraints?.hasTightHeight ?? false) || - constraints.hasTightHeight)); + final bool boundedOnly = maxMainSize != null && !(isHorizontal + ? (contentBoxLogicalWidth != null || (contentConstraints?.hasTightWidth ?? false) || constraints.hasTightWidth) + : (contentBoxLogicalHeight != null || (contentConstraints?.hasTightHeight ?? false) || constraints.hasTightHeight)); double initialFreeSpace = 0; if (maxMainSize != null) { initialFreeSpace = maxMainSize - totalSpace; @@ -5006,19 +4465,16 @@ class RenderFlexLayout extends RenderLayoutBox { // Only honor a definite author-specified min-main-size on the container. double containerStyleMin = 0.0; if (isHorizontal) { - if (renderStyle.minWidth.isNotAuto) - containerStyleMin = renderStyle.minWidth.computedValue; + if (renderStyle.minWidth.isNotAuto) containerStyleMin = renderStyle.minWidth.computedValue; } else { - if (renderStyle.minHeight.isNotAuto) - containerStyleMin = renderStyle.minHeight.computedValue; + if (renderStyle.minHeight.isNotAuto) containerStyleMin = renderStyle.minHeight.computedValue; } // If a definite min is set, treat that as the available main size headroom. // Otherwise, keep the container's main size content-driven with zero // distributable positive free space. if (containerStyleMin > 0) { - final double inferredMain = - math.max(containerStyleMin, layoutContentMainSize); + final double inferredMain = math.max(containerStyleMin, layoutContentMainSize); maxMainSize = inferredMain; initialFreeSpace = inferredMain - totalSpace; } else { @@ -5028,18 +4484,15 @@ class RenderFlexLayout extends RenderLayoutBox { } } - double layoutContentMainSize = - isHorizontal ? contentSize.width : contentSize.height; + double layoutContentMainSize = isHorizontal ? contentSize.width : contentSize.height; // Only consider an author-specified (definite) min-main-size on the flex container here. // Do not use the automatic min size, which includes padding/border, to synthesize // positive free space; that incorrectly inflates the container (e.g., to 360). double containerStyleMin = 0.0; if (isHorizontal) { - if (renderStyle.minWidth.isNotAuto) - containerStyleMin = renderStyle.minWidth.computedValue; + if (renderStyle.minWidth.isNotAuto) containerStyleMin = renderStyle.minWidth.computedValue; } else { - if (renderStyle.minHeight.isNotAuto) - containerStyleMin = renderStyle.minHeight.computedValue; + if (renderStyle.minHeight.isNotAuto) containerStyleMin = renderStyle.minHeight.computedValue; } // Adapt free space only when the container has a definite CSS min-main-size. if (initialFreeSpace == 0) { @@ -5049,18 +4502,12 @@ class RenderFlexLayout extends RenderLayoutBox { if (maxMainSize < minTarget) { maxMainSize = minTarget; - double maxMainConstraints = _isHorizontalFlexDirection - ? contentConstraints!.maxWidth - : contentConstraints!.maxHeight; + double maxMainConstraints = + _isHorizontalFlexDirection ? contentConstraints!.maxWidth : contentConstraints!.maxHeight; // determining isScrollingContentBox is to reduce the scope of influence - if (renderStyle.isSelfScrollingContainer() && - maxMainConstraints.isFinite) { - maxMainSize = totalFlexShrink > 0 - ? math.min(maxMainSize, maxMainConstraints) - : maxMainSize; - maxMainSize = totalFlexGrow > 0 - ? math.max(maxMainSize, maxMainConstraints) - : maxMainSize; + if (renderStyle.isSelfScrollingContainer() && maxMainConstraints.isFinite) { + maxMainSize = totalFlexShrink > 0 ? math.min(maxMainSize, maxMainConstraints) : maxMainSize; + maxMainSize = totalFlexGrow > 0 ? math.max(maxMainSize, maxMainConstraints) : maxMainSize; } initialFreeSpace = maxMainSize - totalSpace; @@ -5083,11 +4530,9 @@ class RenderFlexLayout extends RenderLayoutBox { // a definite CSS min-main-size exists on the flex container itself. double containerStyleMin = 0.0; if (isHorizontal) { - if (renderStyle.minWidth.isNotAuto) - containerStyleMin = renderStyle.minWidth.computedValue; + if (renderStyle.minWidth.isNotAuto) containerStyleMin = renderStyle.minWidth.computedValue; } else { - if (renderStyle.minHeight.isNotAuto) - containerStyleMin = renderStyle.minHeight.computedValue; + if (renderStyle.minHeight.isNotAuto) containerStyleMin = renderStyle.minHeight.computedValue; } if (containerStyleMin <= 0 || containerStyleMin <= totalSpace) { @@ -5117,8 +4562,7 @@ class RenderFlexLayout extends RenderLayoutBox { flexShrink: metrics.totalFlexShrink, ); // Loop flex items to resolve flexible length of flex items with flex factor. - while ( - _resolveFlexibleLengths(metrics, totalFlexFactor, usedFreeSpace)) {} + while (_resolveFlexibleLengths(metrics, totalFlexFactor, usedFreeSpace)) {} } // Phase 1 — Relayout each item with its resolved main size only. @@ -5136,8 +4580,7 @@ class RenderFlexLayout extends RenderLayoutBox { // Determine used main size from the flexible lengths result, if any. double? childFlexedMainSize; - if ((isFlexGrow && runChild.flexGrow > 0) || - (isFlexShrink && runChild.flexShrink > 0)) { + if ((isFlexGrow && runChild.flexGrow > 0) || (isFlexShrink && runChild.flexShrink > 0)) { childFlexedMainSize = runChild.flexedMainSize; } @@ -5149,9 +4592,7 @@ class RenderFlexLayout extends RenderLayoutBox { bool needsLayout = (childFlexedMainSize != null) || (effectiveChild.needsRelayout) || (_childrenRequirePostMeasureLayout[child] == true); - if (!needsLayout && - desiredPreservedMain != null && - (desiredPreservedMain != childOldMainSize)) { + if (!needsLayout && desiredPreservedMain != null && (desiredPreservedMain != childOldMainSize)) { needsLayout = true; } if (!needsLayout && desiredPreservedMain != null) { @@ -5175,8 +4616,7 @@ class RenderFlexLayout extends RenderLayoutBox { if (!needsLayout && !_isHorizontalFlexDirection) { final bool childCrossAuto = effectiveChild.renderStyle.width.isAuto; final bool noStretch = !_needToStretchChildCrossSize(effectiveChild); - final double availCross = - contentConstraints?.maxWidth ?? double.infinity; + final double availCross = contentConstraints?.maxWidth ?? double.infinity; if (childCrossAuto && noStretch && availCross.isFinite) { // Compare against the border-box width measured during intrinsic pass. final double measuredBorderW = _getChildSize(effectiveChild)!.width; @@ -5213,10 +4653,9 @@ class RenderFlexLayout extends RenderLayoutBox { double? childStretchedCrossSize; if (_needToStretchChildCrossSize(effectiveChild)) { - childStretchedCrossSize = _getChildStretchedCrossSize( - effectiveChild, metrics.crossAxisExtent, runBetweenSpace); - if (effectiveChild is RenderLayoutBox && - effectiveChild.isNegativeMarginChangeHSize) { + childStretchedCrossSize = + _getChildStretchedCrossSize(effectiveChild, metrics.crossAxisExtent, runBetweenSpace); + if (effectiveChild is RenderLayoutBox && effectiveChild.isNegativeMarginChangeHSize) { double childCrossAxisMargin = _isHorizontalFlexDirection ? effectiveChild.renderStyle.margin.vertical : effectiveChild.renderStyle.margin.horizontal; @@ -5273,10 +4712,8 @@ class RenderFlexLayout extends RenderLayoutBox { double childCrossMargin = 0; if (child is RenderBoxModel) { childCrossMargin = isHorizontal - ? child.renderStyle.marginTop.computedValue + - child.renderStyle.marginBottom.computedValue - : child.renderStyle.marginLeft.computedValue + - child.renderStyle.marginRight.computedValue; + ? child.renderStyle.marginTop.computedValue + child.renderStyle.marginBottom.computedValue + : child.renderStyle.marginLeft.computedValue + child.renderStyle.marginRight.computedValue; } double childCrossExtent = childCrossSize + childCrossMargin; @@ -5296,7 +4733,8 @@ class RenderFlexLayout extends RenderLayoutBox { // Vertical align is only valid for inline box. // Baseline alignment in column direction behave the same as flex-start. AlignSelf alignSelf = _getAlignSelf(child); - bool isBaselineAlign = alignSelf == AlignSelf.baseline || + bool isBaselineAlign = + alignSelf == AlignSelf.baseline || alignSelf == AlignSelf.lastBaseline || renderStyle.alignItems == AlignItems.baseline || renderStyle.alignItems == AlignItems.lastBaseline; @@ -5329,8 +4767,7 @@ class RenderFlexLayout extends RenderLayoutBox { // Get constraints of flex items which needs to change size due to // flex-grow/flex-shrink or align-items stretch. - BoxConstraints _getChildAdjustedConstraints( - RenderBoxModel child, + BoxConstraints _getChildAdjustedConstraints(RenderBoxModel child, double? childFlexedMainSize, double? childStretchedCrossSize, int lineChildrenCount, @@ -5354,15 +4791,12 @@ class RenderFlexLayout extends RenderLayoutBox { } } - if (child.renderStyle.isSelfRenderReplaced() && - child.renderStyle.aspectRatio != null) { - _overrideReplacedChildLength( - child, childFlexedMainSize, childStretchedCrossSize); + if (child.renderStyle.isSelfRenderReplaced() && child.renderStyle.aspectRatio != null) { + _overrideReplacedChildLength(child, childFlexedMainSize, childStretchedCrossSize); } // Use stored percentage constraints if available, otherwise use current constraints - BoxConstraints oldConstraints = - _childrenOldConstraints[child] ?? child.constraints; + BoxConstraints oldConstraints = _childrenOldConstraints[child] ?? child.constraints; // Row flex container + pure cross-axis stretch: // Preserve the flex-resolved main size (oldConstraints.min/maxWidth) and @@ -5401,9 +4835,8 @@ class RenderFlexLayout extends RenderLayoutBox { if (contentW != null && contentW.isFinite) { final double pad = child.renderStyle.paddingLeft.computedValue + child.renderStyle.paddingRight.computedValue; - final double border = - child.renderStyle.effectiveBorderLeftWidth.computedValue + - child.renderStyle.effectiveBorderRightWidth.computedValue; + final double border = child.renderStyle.effectiveBorderLeftWidth.computedValue + + child.renderStyle.effectiveBorderRightWidth.computedValue; return math.max(0, contentW + pad + border); } return math.max(0, oldConstraints.maxWidth); @@ -5416,9 +4849,8 @@ class RenderFlexLayout extends RenderLayoutBox { if (contentH != null && contentH.isFinite) { final double pad = child.renderStyle.paddingTop.computedValue + child.renderStyle.paddingBottom.computedValue; - final double border = - child.renderStyle.effectiveBorderTopWidth.computedValue + - child.renderStyle.effectiveBorderBottomWidth.computedValue; + final double border = child.renderStyle.effectiveBorderTopWidth.computedValue + + child.renderStyle.effectiveBorderBottomWidth.computedValue; return math.max(0, contentH + pad + border); } return math.max(0, oldConstraints.maxHeight); @@ -5433,14 +4865,10 @@ class RenderFlexLayout extends RenderLayoutBox { double minConstraintWidth = child.hasOverrideContentLogicalWidth ? safeUsedBorderBoxWidth() - : (oldConstraints.minWidth > maxConstraintWidth - ? maxConstraintWidth - : oldConstraints.minWidth); + : (oldConstraints.minWidth > maxConstraintWidth ? maxConstraintWidth : oldConstraints.minWidth); double minConstraintHeight = child.hasOverrideContentLogicalHeight ? safeUsedBorderBoxHeight() - : (oldConstraints.minHeight > maxConstraintHeight - ? maxConstraintHeight - : oldConstraints.minHeight); + : (oldConstraints.minHeight > maxConstraintHeight ? maxConstraintHeight : oldConstraints.minHeight); // If the flex item has a definite height in a row-direction container, // lock the child's height to the used border-box height. This prevents @@ -5497,16 +4925,12 @@ class RenderFlexLayout extends RenderLayoutBox { // overflowing. This mirrors practical browser behavior for scrollable widgets when // author intent is flex: 1; min-height: 0 under a max-height container. if (!_isHorizontalFlexDirection) { - final bool containerBounded = - (contentConstraints?.hasBoundedHeight ?? false) && - (contentConstraints?.maxHeight.isFinite ?? false); + final bool containerBounded = (contentConstraints?.hasBoundedHeight ?? false) && + (contentConstraints?.maxHeight.isFinite ?? false); if (containerBounded) { final double cap = contentConstraints!.maxHeight; - final bool childIsWidget = - child is RenderWidget || child.renderStyle.target is WidgetElement; - if (childIsWidget && - preserveMainAxisSize != null && - preserveMainAxisSize > cap) { + final bool childIsWidget = child is RenderWidget || child.renderStyle.target is WidgetElement; + if (childIsWidget && preserveMainAxisSize != null && preserveMainAxisSize > cap) { minConstraintHeight = cap; maxConstraintHeight = cap; } @@ -5520,12 +4944,9 @@ class RenderFlexLayout extends RenderLayoutBox { // Do not apply this optimization to replaced elements; their aspect-ratio handling // and intrinsic sizing already provide stable behavior, and locking can produce // intermediate widths that conflict with later stretch. - if (!_isHorizontalFlexDirection && - childStretchedCrossSize == null && - !child.renderStyle.isSelfRenderReplaced()) { + if (!_isHorizontalFlexDirection && childStretchedCrossSize == null && !child.renderStyle.isSelfRenderReplaced()) { final bool childCrossAuto = child.renderStyle.width.isAuto; - final bool childCrossPercent = - child.renderStyle.width.type == CSSLengthType.PERCENTAGE; + final bool childCrossPercent = child.renderStyle.width.type == CSSLengthType.PERCENTAGE; // Determine the effective cross-axis alignment for this child. final AlignSelf selfAlign = _getAlignSelf(child); @@ -5539,8 +4960,7 @@ class RenderFlexLayout extends RenderLayoutBox { // column-direction flex items with non-stretch alignment. if (childCrossAuto && !isStretchAlignment) { final WhiteSpace ws = child.renderStyle.whiteSpace; - final bool allowOverflowCross = - ws == WhiteSpace.pre || ws == WhiteSpace.nowrap; + final bool allowOverflowCross = ws == WhiteSpace.pre || ws == WhiteSpace.nowrap; if (allowOverflowCross) { // For unbreakable text (`pre`/`nowrap`), let the item overflow the flex container in // the cross axis by giving it an unbounded width constraint. This allows the span @@ -5552,18 +4972,15 @@ class RenderFlexLayout extends RenderLayoutBox { // Compute shrink-to-fit width in the cross axis for column flex items: // used = min(max(min-content, available), max-content). // Work in content-box, then convert to border-box by adding padding+border. - final double paddingBorderH = child.renderStyle.padding.horizontal + - child.renderStyle.border.horizontal; + final double paddingBorderH = child.renderStyle.padding.horizontal + child.renderStyle.border.horizontal; // Min-content contribution (content-box). final double minContentCB = child.minContentWidth; // Max-content contribution (content-box). Prefer IFC paragraph max-intrinsic width for flow content. double maxContentCB = minContentCB; // fallback - if (child is RenderFlowLayout && - child.inlineFormattingContext != null) { - final double paraMax = - child.inlineFormattingContext!.paragraphMaxIntrinsicWidth; + if (child is RenderFlowLayout && child.inlineFormattingContext != null) { + final double paraMax = child.inlineFormattingContext!.paragraphMaxIntrinsicWidth; if (paraMax.isFinite && paraMax > 0) maxContentCB = paraMax; } @@ -5573,25 +4990,20 @@ class RenderFlexLayout extends RenderLayoutBox { // cross axis. Subtract positive start/end margins to get the space // available to the item’s border-box for shrink-to-fit width. double availableCross = double.infinity; - if (contentConstraints != null && - contentConstraints!.maxWidth.isFinite) { + if (contentConstraints != null && contentConstraints!.maxWidth.isFinite) { availableCross = contentConstraints!.maxWidth; } else { // Fallback to current laid-out content width when known. - final double borderH = - renderStyle.effectiveBorderLeftWidth.computedValue + - renderStyle.effectiveBorderRightWidth.computedValue; + final double borderH = renderStyle.effectiveBorderLeftWidth.computedValue + + renderStyle.effectiveBorderRightWidth.computedValue; final double fallback = math.max(0.0, size.width - borderH); if (fallback.isFinite && fallback > 0) availableCross = fallback; } // Subtract cross-axis margins (positive values only) from available width. if (availableCross.isFinite) { - final double startMargin = - _flowAwareChildCrossAxisMargin(child) ?? 0; - final double endMargin = - _flowAwareChildCrossAxisMargin(child, isEnd: true) ?? 0; - final double marginDeduction = - math.max(0.0, startMargin) + math.max(0.0, endMargin); + final double startMargin = _flowAwareChildCrossAxisMargin(child) ?? 0; + final double endMargin = _flowAwareChildCrossAxisMargin(child, isEnd: true) ?? 0; + final double marginDeduction = math.max(0.0, startMargin) + math.max(0.0, endMargin); availableCross = math.max(0.0, availableCross - marginDeduction); } @@ -5601,11 +5013,8 @@ class RenderFlexLayout extends RenderLayoutBox { // regressing centering cases (e.g., column-wrap with align-self:center). if (maxContentCB <= minContentCB + 0.5) { final double priorBorderW = _getChildSize(child)!.width; - final double priorContentW = math.max( - 0.0, - priorBorderW - - (child.renderStyle.padding.horizontal + - child.renderStyle.border.horizontal)); + final double priorContentW = + math.max(0.0, priorBorderW - (child.renderStyle.padding.horizontal + child.renderStyle.border.horizontal)); if (priorContentW.isFinite && priorContentW > minContentCB) { maxContentCB = priorContentW; } @@ -5613,15 +5022,12 @@ class RenderFlexLayout extends RenderLayoutBox { // Convert to border-box for comparison with constraints we apply to the child. final double minBorder = math.max(0.0, minContentCB + paddingBorderH); - final double maxBorder = - math.max(minBorder, maxContentCB + paddingBorderH); - final double availBorder = - availableCross.isFinite ? availableCross : double.infinity; + final double maxBorder = math.max(minBorder, maxContentCB + paddingBorderH); + final double availBorder = availableCross.isFinite ? availableCross : double.infinity; double shrinkBorderW = maxBorder; if (availBorder.isFinite) { - shrinkBorderW = - math.min(math.max(minBorder, availBorder), maxBorder); + shrinkBorderW = math.min(math.max(minBorder, availBorder), maxBorder); } // Respect the child’s own min/max-width caps. @@ -5630,25 +5036,21 @@ class RenderFlexLayout extends RenderLayoutBox { if (child.renderStyle.minWidth.isNotAuto) { if (child.renderStyle.minWidth.type == CSSLengthType.PERCENTAGE) { if (availableCross.isFinite) { - final double usedMin = - (child.renderStyle.minWidth.value ?? 0) * availableCross; + final double usedMin = (child.renderStyle.minWidth.value ?? 0) * availableCross; shrinkBorderW = math.max(shrinkBorderW, usedMin); } } else { - shrinkBorderW = math.max( - shrinkBorderW, child.renderStyle.minWidth.computedValue); + shrinkBorderW = math.max(shrinkBorderW, child.renderStyle.minWidth.computedValue); } } if (child.renderStyle.maxWidth.isNotNone) { if (child.renderStyle.maxWidth.type == CSSLengthType.PERCENTAGE) { if (availableCross.isFinite) { - final double usedMax = - (child.renderStyle.maxWidth.value ?? 0) * availableCross; + final double usedMax = (child.renderStyle.maxWidth.value ?? 0) * availableCross; shrinkBorderW = math.min(shrinkBorderW, usedMax); } } else { - shrinkBorderW = math.min( - shrinkBorderW, child.renderStyle.maxWidth.computedValue); + shrinkBorderW = math.min(shrinkBorderW, child.renderStyle.maxWidth.computedValue); } } @@ -5663,24 +5065,17 @@ class RenderFlexLayout extends RenderLayoutBox { // (or when alignment falls back to stretch). This preserves previous layout for // stretch cases and avoids regressions. double fixedW; - if (child.renderStyle.isSelfRenderReplaced() && - child.renderStyle.aspectRatio != null) { - final double usedBorderBoxH = - child.renderStyle.borderBoxLogicalHeight ?? - _getChildSize(child)!.height; + if (child.renderStyle.isSelfRenderReplaced() && child.renderStyle.aspectRatio != null) { + final double usedBorderBoxH = child.renderStyle.borderBoxLogicalHeight ?? _getChildSize(child)!.height; fixedW = usedBorderBoxH * child.renderStyle.aspectRatio!; } else { fixedW = _getChildSize(child)!.width; } - final double containerCrossMax = - contentConstraints?.maxWidth ?? double.infinity; + final double containerCrossMax = contentConstraints?.maxWidth ?? double.infinity; final double containerContentW = containerCrossMax.isFinite ? containerCrossMax - : math.max( - 0.0, - size.width - - (renderStyle.effectiveBorderLeftWidth.computedValue + - renderStyle.effectiveBorderRightWidth.computedValue)); + : math.max(0.0, size.width - (renderStyle.effectiveBorderLeftWidth.computedValue + + renderStyle.effectiveBorderRightWidth.computedValue)); double styleMinW = 0.0; final CSSLengthValue minWLen = child.renderStyle.minWidth; @@ -5698,8 +5093,7 @@ class RenderFlexLayout extends RenderLayoutBox { } fixedW = fixedW.clamp(styleMinW, styleMaxW); - if (containerContentW.isFinite) - fixedW = math.min(fixedW, containerContentW); + if (containerContentW.isFinite) fixedW = math.min(fixedW, containerContentW); if (fixedW.isFinite && fixedW > 0) { minConstraintWidth = fixedW; @@ -5711,29 +5105,22 @@ class RenderFlexLayout extends RenderLayoutBox { // once it becomes known (second layout pass). This matches CSS flex item percentage resolution // in column-direction containers. double containerContentW; - if (contentConstraints != null && - contentConstraints!.maxWidth.isFinite) { + if (contentConstraints != null && contentConstraints!.maxWidth.isFinite) { containerContentW = contentConstraints!.maxWidth; } else { - final double borderH = - renderStyle.effectiveBorderLeftWidth.computedValue + - renderStyle.effectiveBorderRightWidth.computedValue; + final double borderH = renderStyle.effectiveBorderLeftWidth.computedValue + + renderStyle.effectiveBorderRightWidth.computedValue; containerContentW = math.max(0, size.width - borderH); } final double percent = child.renderStyle.width.value ?? 0; - double childBorderBoxW = - containerContentW.isFinite ? (containerContentW * percent) : 0; - if (!childBorderBoxW.isFinite || childBorderBoxW < 0) - childBorderBoxW = 0; - - if (child.renderStyle.maxWidth.isNotNone && - child.renderStyle.maxWidth.type != CSSLengthType.PERCENTAGE) { - childBorderBoxW = math.min( - childBorderBoxW, child.renderStyle.maxWidth.computedValue); + double childBorderBoxW = containerContentW.isFinite ? (containerContentW * percent) : 0; + if (!childBorderBoxW.isFinite || childBorderBoxW < 0) childBorderBoxW = 0; + + if (child.renderStyle.maxWidth.isNotNone && child.renderStyle.maxWidth.type != CSSLengthType.PERCENTAGE) { + childBorderBoxW = math.min(childBorderBoxW, child.renderStyle.maxWidth.computedValue); } if (child.renderStyle.minWidth.isNotAuto) { - childBorderBoxW = math.max( - childBorderBoxW, child.renderStyle.minWidth.computedValue); + childBorderBoxW = math.max(childBorderBoxW, child.renderStyle.minWidth.computedValue); } minConstraintWidth = childBorderBoxW; @@ -5745,14 +5132,10 @@ class RenderFlexLayout extends RenderLayoutBox { // For replaced elements in a column flex container during phase 1 (no cross stretch yet), // cap the available cross size by the container’s content-box width so the intrinsic sizing // does not expand to unconstrained widths (e.g., viewport width) before the stretch phase. - if (!_isHorizontalFlexDirection && - child.renderStyle.isSelfRenderReplaced() && - childStretchedCrossSize == null) { - final double containerCrossMax = - contentConstraints?.maxWidth ?? double.infinity; + if (!_isHorizontalFlexDirection && child.renderStyle.isSelfRenderReplaced() && childStretchedCrossSize == null) { + final double containerCrossMax = contentConstraints?.maxWidth ?? double.infinity; if (containerCrossMax.isFinite) { - if (maxConstraintWidth.isInfinite || - maxConstraintWidth > containerCrossMax) { + if (maxConstraintWidth.isInfinite || maxConstraintWidth > containerCrossMax) { maxConstraintWidth = containerCrossMax; } if (minConstraintWidth > maxConstraintWidth) { @@ -5764,22 +5147,19 @@ class RenderFlexLayout extends RenderLayoutBox { // For elements or any inline-level elements in horizontal flex layout, // avoid tight height constraints during secondary layout passes. // This allows text to properly reflow and adjust its height when width changes. - bool isTextElement = child.renderStyle.isSelfRenderWidget() && - child.renderStyle.target is WebFTextElement; - bool isInlineElementWithText = - (child.renderStyle.display == CSSDisplay.inline || - child.renderStyle.display == CSSDisplay.inlineBlock || - child.renderStyle.display == CSSDisplay.inlineFlex) && - (child.renderStyle.isSelfRenderFlowLayout() || - child.renderStyle.isSelfRenderFlexLayout()); - bool isAutoHeightLayoutContainer = child.renderStyle.height.isAuto && + bool isTextElement = child.renderStyle.isSelfRenderWidget() && child.renderStyle.target is WebFTextElement; + bool isInlineElementWithText = (child.renderStyle.display == CSSDisplay.inline || + child.renderStyle.display == CSSDisplay.inlineBlock || + child.renderStyle.display == CSSDisplay.inlineFlex) && + (child.renderStyle.isSelfRenderFlowLayout() || child.renderStyle.isSelfRenderFlexLayout()); + bool isAutoHeightLayoutContainer = + child.renderStyle.height.isAuto && (child.renderStyle.isSelfRenderFlowLayout() || child.renderStyle.isSelfRenderFlexLayout()); // Block-level flex items whose contents form an inline formatting context (e.g., a
with only text) // also need height to be unconstrained on the secondary pass so text can wrap after flex-shrink. // This mirrors browser behavior: first resolve the used main size, then measure cross size with auto height. - bool establishesIFC = - child.renderStyle.shouldEstablishInlineFormattingContext(); + bool establishesIFC = child.renderStyle.shouldEstablishInlineFormattingContext(); bool isSecondaryLayoutPass = child.hasSize; // Allow dynamic height adjustment during secondary layout when width has changed and height is auto @@ -5791,8 +5171,7 @@ class RenderFlexLayout extends RenderLayoutBox { isAutoHeightLayoutContainer) && // For non-flexed items, only allow when this is the only item on the line // so the line cross-size is content-driven. - (childFlexedMainSize != null || - (preserveMainAxisSize != null && lineChildrenCount == 1)) && + (childFlexedMainSize != null || (preserveMainAxisSize != null && lineChildrenCount == 1)) && // Layout containers with auto height need a chance to grow past the // previous stretched cross size when their own contents change. (childStretchedCrossSize == null || @@ -5806,8 +5185,7 @@ class RenderFlexLayout extends RenderLayoutBox { // margins and padding are preserved while still allowing it to expand beyond // the stretched size if its contents require it. if (childStretchedCrossSize != null && childStretchedCrossSize > 0) { - minConstraintHeight = - math.max(minConstraintHeight, childStretchedCrossSize); + minConstraintHeight = math.max(minConstraintHeight, childStretchedCrossSize); } else { minConstraintHeight = 0; } @@ -5827,10 +5205,8 @@ class RenderFlexLayout extends RenderLayoutBox { if (!child.renderStyle.paddingBottom.isAuto) { contentMinHeight += child.renderStyle.paddingBottom.computedValue; } - contentMinHeight += - child.renderStyle.effectiveBorderTopWidth.computedValue; - contentMinHeight += - child.renderStyle.effectiveBorderBottomWidth.computedValue; + contentMinHeight += child.renderStyle.effectiveBorderTopWidth.computedValue; + contentMinHeight += child.renderStyle.effectiveBorderBottomWidth.computedValue; // Allow child to expand beyond parent's maxHeight if content requires it // This matches browser behavior where content can overflow constrained parents @@ -5874,23 +5250,19 @@ class RenderFlexLayout extends RenderLayoutBox { final bool isReplaced = child.renderStyle.isSelfRenderReplaced(); final double childFlexGrow = _getFlexGrow(child); final double childFlexShrink = _getFlexShrink(child); - final bool isFlexNone = - childFlexGrow == 0 && childFlexShrink == 0; // flex: none - if (hasDefiniteFlexBasis || - (child.renderStyle.width.isAuto && !isReplaced)) { + final bool isFlexNone = childFlexGrow == 0 && childFlexShrink == 0; // flex: none + if (hasDefiniteFlexBasis || (child.renderStyle.width.isAuto && !isReplaced)) { if (isFlexNone) { // For flex: none items, do not constrain to the container width. // Let the item keep its preserved (intrinsic) width and overflow if necessary. minConstraintWidth = preserveMainAxisSize; maxConstraintWidth = preserveMainAxisSize; } else { - final double containerAvail = - contentConstraints?.maxWidth ?? double.infinity; + final double containerAvail = contentConstraints?.maxWidth ?? double.infinity; if (containerAvail.isFinite) { double cap = preserveMainAxisSize; // Also honor the child’s own definite (non-percentage) max-width if any. - if (child.renderStyle.maxWidth.isNotNone && - child.renderStyle.maxWidth.type != CSSLengthType.PERCENTAGE) { + if (child.renderStyle.maxWidth.isNotNone && child.renderStyle.maxWidth.type != CSSLengthType.PERCENTAGE) { cap = math.min(cap, child.renderStyle.maxWidth.computedValue); } cap = math.min(cap, containerAvail); @@ -5913,10 +5285,9 @@ class RenderFlexLayout extends RenderLayoutBox { // container must not force a tight height on the item; allow the child to // reflow under the container's bounded height instead of freezing to the // intrinsic height from PASS 2. - final bool containerBoundedOnly = - (contentConstraints?.hasBoundedHeight ?? false) && - !(contentConstraints?.hasTightHeight ?? false) && - renderStyle.contentBoxLogicalHeight == null; + final bool containerBoundedOnly = (contentConstraints?.hasBoundedHeight ?? false) && + !(contentConstraints?.hasTightHeight ?? false) && + renderStyle.contentBoxLogicalHeight == null; // Avoid over-constraining text reflow cases by applying only when the // intrinsic pass forced a tight zero height or when the basis is definite @@ -5951,11 +5322,9 @@ class RenderFlexLayout extends RenderLayoutBox { // When replaced element is stretched or shrinked only on one axis and // length is not specified on the other axis, the length needs to be // overrided in the other axis. - void _overrideReplacedChildLength( - RenderBoxModel child, - double? childFlexedMainSize, - double? childStretchedCrossSize, - ) { + void _overrideReplacedChildLength(RenderBoxModel child, + double? childFlexedMainSize, + double? childStretchedCrossSize,) { assert(child.renderStyle.isSelfRenderReplaced()); if (childFlexedMainSize != null && childStretchedCrossSize == null) { if (_isHorizontalFlexDirection) { @@ -5975,48 +5344,38 @@ class RenderFlexLayout extends RenderLayoutBox { } // Override replaced child height when its height is auto. - void _overrideReplacedChildHeight( - RenderBoxModel child, - ) { + void _overrideReplacedChildHeight(RenderBoxModel child,) { assert(child.renderStyle.isSelfRenderReplaced()); if (child.renderStyle.height.isAuto) { double maxConstraintWidth = child.renderStyle.borderBoxLogicalWidth!; - double maxConstraintHeight = - maxConstraintWidth / child.renderStyle.aspectRatio!; + double maxConstraintHeight = maxConstraintWidth / child.renderStyle.aspectRatio!; // Clamp replaced element height by min/max height. if (child.renderStyle.minHeight.isNotAuto) { double minHeight = child.renderStyle.minHeight.computedValue; - maxConstraintHeight = - maxConstraintHeight < minHeight ? minHeight : maxConstraintHeight; + maxConstraintHeight = maxConstraintHeight < minHeight ? minHeight : maxConstraintHeight; } if (child.renderStyle.maxHeight.isNotNone) { double maxHeight = child.renderStyle.maxHeight.computedValue; - maxConstraintHeight = - maxConstraintHeight > maxHeight ? maxHeight : maxConstraintHeight; + maxConstraintHeight = maxConstraintHeight > maxHeight ? maxHeight : maxConstraintHeight; } _overrideChildContentBoxLogicalHeight(child, maxConstraintHeight); } } // Override replaced child width when its width is auto. - void _overrideReplacedChildWidth( - RenderBoxModel child, - ) { + void _overrideReplacedChildWidth(RenderBoxModel child,) { assert(child.renderStyle.isSelfRenderReplaced()); if (child.renderStyle.width.isAuto) { double maxConstraintHeight = child.renderStyle.borderBoxLogicalHeight!; - double maxConstraintWidth = - maxConstraintHeight * child.renderStyle.aspectRatio!; + double maxConstraintWidth = maxConstraintHeight * child.renderStyle.aspectRatio!; // Clamp replaced element width by min/max width. if (child.renderStyle.minWidth.isNotAuto) { double minWidth = child.renderStyle.minWidth.computedValue; - maxConstraintWidth = - maxConstraintWidth < minWidth ? minWidth : maxConstraintWidth; + maxConstraintWidth = maxConstraintWidth < minWidth ? minWidth : maxConstraintWidth; } if (child.renderStyle.maxWidth.isNotNone) { double maxWidth = child.renderStyle.maxWidth.computedValue; - maxConstraintWidth = - maxConstraintWidth > maxWidth ? maxWidth : maxConstraintWidth; + maxConstraintWidth = maxConstraintWidth > maxWidth ? maxWidth : maxConstraintWidth; } _overrideChildContentBoxLogicalWidth(child, maxConstraintWidth); } @@ -6024,15 +5383,13 @@ class RenderFlexLayout extends RenderLayoutBox { // Override content box logical width of child when flex-grow/flex-shrink/align-items has changed // child's size. - void _overrideChildContentBoxLogicalWidth( - RenderBoxModel child, double maxConstraintWidth) { + void _overrideChildContentBoxLogicalWidth(RenderBoxModel child, double maxConstraintWidth) { // Deflating padding/border can yield a negative content-box when the // assigned border-box is smaller than padding+border. CSS forbids // negative content sizes; clamp to zero so downstream layout (e.g., // intrinsic measurement and alignment) receives a sane, non-negative // logical size. - double deflated = - child.renderStyle.deflatePaddingBorderWidth(maxConstraintWidth); + double deflated = child.renderStyle.deflatePaddingBorderWidth(maxConstraintWidth); if (deflated.isFinite && deflated < 0) deflated = 0; child.renderStyle.contentBoxLogicalWidth = deflated; child.hasOverrideContentLogicalWidth = true; @@ -6040,22 +5397,18 @@ class RenderFlexLayout extends RenderLayoutBox { // Override content box logical height of child when flex-grow/flex-shrink/align-items has changed // child's size. - void _overrideChildContentBoxLogicalHeight( - RenderBoxModel child, double maxConstraintHeight) { + void _overrideChildContentBoxLogicalHeight(RenderBoxModel child, double maxConstraintHeight) { // See width counterpart: guard against negative content-box heights // when padding/border exceed the assigned border-box. Use zero to // represent the collapsed content box per spec. - double deflated = - child.renderStyle.deflatePaddingBorderHeight(maxConstraintHeight); + double deflated = child.renderStyle.deflatePaddingBorderHeight(maxConstraintHeight); if (deflated.isFinite && deflated < 0) deflated = 0; child.renderStyle.contentBoxLogicalHeight = deflated; child.hasOverrideContentLogicalHeight = true; } // Set flex container size according to children size. - void _setContainerSize( - List<_RunMetrics> runMetrics, - ) { + void _setContainerSize(List<_RunMetrics> runMetrics,) { if (runMetrics.isEmpty) { _setContainerSizeWithNoChild(); return; @@ -6066,16 +5419,13 @@ class RenderFlexLayout extends RenderLayoutBox { // mainAxis gaps are already included in metrics.mainAxisExtent after PASS 3. // No need to add them again as this would double-count and cause incorrect sizing. - double contentWidth = - _isHorizontalFlexDirection ? runMaxMainSize : runCrossSize; - double contentHeight = - _isHorizontalFlexDirection ? runCrossSize : runMaxMainSize; + double contentWidth = _isHorizontalFlexDirection ? runMaxMainSize : runCrossSize; + double contentHeight = _isHorizontalFlexDirection ? runCrossSize : runMaxMainSize; // Respect specified cross size (height for row, width for column) without growing the container. // This allows flex items to overflow when their content is taller/wider than the container. if (_isHorizontalFlexDirection) { - final double? specifiedContentHeight = - renderStyle.contentBoxLogicalHeight; + final double? specifiedContentHeight = renderStyle.contentBoxLogicalHeight; if (specifiedContentHeight != null) { contentHeight = specifiedContentHeight; } @@ -6128,9 +5478,8 @@ class RenderFlexLayout extends RenderLayoutBox { double childMarginBottom = child.renderStyle.marginBottom.computedValue; double childMarginLeft = child.renderStyle.marginLeft.computedValue; double childMarginRight = child.renderStyle.marginRight.computedValue; - runChildMainSize += _isHorizontalFlexDirection - ? childMarginLeft + childMarginRight - : childMarginTop + childMarginBottom; + runChildMainSize += + _isHorizontalFlexDirection ? childMarginLeft + childMarginRight : childMarginTop + childMarginBottom; } runMainExtent += runChildMainSize; } @@ -6139,9 +5488,7 @@ class RenderFlexLayout extends RenderLayoutBox { // Get auto min size in the main axis which equals the main axis size of its contents. // https://www.w3.org/TR/css-sizing-3/#automatic-minimum-size - double _getMainAxisAutoSize( - List<_RunMetrics> runMetrics, - ) { + double _getMainAxisAutoSize(List<_RunMetrics> runMetrics,) { double autoMinSize = 0; // Main size of each run. @@ -6165,8 +5512,7 @@ class RenderFlexLayout extends RenderLayoutBox { for (final _RunChild runChild in runChildren) { final RenderBox child = runChild.child; final Size childSize = _getChildSize(child)!; - final double runChildCrossSize = - _isHorizontalFlexDirection ? childSize.height : childSize.width; + final double runChildCrossSize = _isHorizontalFlexDirection ? childSize.height : childSize.width; runCrossExtent = math.max(runCrossExtent, runChildCrossSize); } runCrossSize.add(runCrossExtent); @@ -6174,9 +5520,7 @@ class RenderFlexLayout extends RenderLayoutBox { // Get auto min size in the cross axis which equals the cross axis size of its contents. // https://www.w3.org/TR/css-sizing-3/#automatic-minimum-size - double _getCrossAxisAutoSize( - List<_RunMetrics> runMetrics, - ) { + double _getCrossAxisAutoSize(List<_RunMetrics> runMetrics,) { double autoMinSize = 0; // Cross size of each run. @@ -6231,8 +5575,7 @@ class RenderFlexLayout extends RenderLayoutBox { final CSSOverflowType overflowX = childRenderStyle.effectiveOverflowX; final CSSOverflowType overflowY = childRenderStyle.effectiveOverflowY; // Only non scroll container need to use scrollable size, otherwise use its own size. - if (overflowX == CSSOverflowType.visible && - overflowY == CSSOverflowType.visible) { + if (overflowX == CSSOverflowType.visible && overflowY == CSSOverflowType.visible) { childScrollableSize = child.scrollableSize; } @@ -6241,42 +5584,33 @@ class RenderFlexLayout extends RenderLayoutBox { // https://www.w3.org/TR/css-overflow-3/#scrollable-overflow-region // Add offset of margin. - childOffsetX += childRenderStyle.marginLeft.computedValue + - childRenderStyle.marginRight.computedValue; - childOffsetY += childRenderStyle.marginTop.computedValue + - childRenderStyle.marginBottom.computedValue; + childOffsetX += childRenderStyle.marginLeft.computedValue + childRenderStyle.marginRight.computedValue; + childOffsetY += childRenderStyle.marginTop.computedValue + childRenderStyle.marginBottom.computedValue; // Add offset of position relative. // Offset of position absolute and fixed is added in layout stage of positioned renderBox. - final Offset? relativeOffset = - CSSPositionedLayout.getRelativeOffset(childRenderStyle); + final Offset? relativeOffset = CSSPositionedLayout.getRelativeOffset(childRenderStyle); if (relativeOffset != null) { childOffsetX += relativeOffset.dx; childOffsetY += relativeOffset.dy; } // Add offset of transform. - final Offset? transformOffset = - child.renderStyle.effectiveTransformOffset; + final Offset? transformOffset = child.renderStyle.effectiveTransformOffset; if (transformOffset != null) { childOffsetX += transformOffset.dx; childOffsetY += transformOffset.dy; childTransformMainOverflow = math.max( 0, - _isHorizontalFlexDirection - ? transformOffset.dx - : transformOffset.dy, + _isHorizontalFlexDirection ? transformOffset.dx : transformOffset.dy, ); } } final Size childSize = _getChildSize(child)!; - final double childBoxMainSize = - _isHorizontalFlexDirection ? childSize.width : childSize.height; - final double childBoxCrossSize = - _isHorizontalFlexDirection ? childSize.height : childSize.width; - final double childCrossOffset = - _isHorizontalFlexDirection ? childOffsetY : childOffsetX; + final double childBoxMainSize = _isHorizontalFlexDirection ? childSize.width : childSize.height; + final double childBoxCrossSize = _isHorizontalFlexDirection ? childSize.height : childSize.width; + final double childCrossOffset = _isHorizontalFlexDirection ? childOffsetY : childOffsetX; final double childScrollableMainExtent = _isHorizontalFlexDirection ? childScrollableSize.width + childTransformMainOverflow : childScrollableSize.height + childTransformMainOverflow; @@ -6301,28 +5635,25 @@ class RenderFlexLayout extends RenderLayoutBox { // instead of the pre-alignment stacked size. This prevents blank trailing // scroll range after children are shifted by negative leading space. final double childScrollableMain = math.max( - 0, - childMainPosition - physicalMainAxisStartBorder, - ) + + 0, + childMainPosition - physicalMainAxisStartBorder, + ) + math.max(childBoxMainSize, childScrollableMainExtent) + childPhysicalMainEndMargin; final double childScrollableCross = math.max( - childBoxCrossSize + childCrossOffset, childScrollableCrossExtent); + childBoxCrossSize + childCrossOffset, + childScrollableCrossExtent); - maxScrollableMainSizeOfLine = - math.max(maxScrollableMainSizeOfLine, childScrollableMain); - maxScrollableCrossSizeInLine = - math.max(maxScrollableCrossSizeInLine, childScrollableCross); + maxScrollableMainSizeOfLine = math.max(maxScrollableMainSizeOfLine, childScrollableMain); + maxScrollableCrossSizeInLine = math.max(maxScrollableCrossSizeInLine, childScrollableCross); // Update running main size for subsequent siblings (border-box size + main-axis margins). double childMainSize = _getMainSize(child); if (child is RenderBoxModel) { if (_isHorizontalFlexDirection) { - childMainSize += child.renderStyle.marginLeft.computedValue + - child.renderStyle.marginRight.computedValue; + childMainSize += child.renderStyle.marginLeft.computedValue + child.renderStyle.marginRight.computedValue; } else { - childMainSize += child.renderStyle.marginTop.computedValue + - child.renderStyle.marginBottom.computedValue; + childMainSize += child.renderStyle.marginTop.computedValue + child.renderStyle.marginBottom.computedValue; } } preSiblingsMainSize += childMainSize; @@ -6333,8 +5664,7 @@ class RenderFlexLayout extends RenderLayoutBox { } // Max scrollable cross size of all the children in the line. - final double maxScrollableCrossSizeOfLine = - preLinesCrossSize + maxScrollableCrossSizeInLine; + final double maxScrollableCrossSizeOfLine = preLinesCrossSize + maxScrollableCrossSizeInLine; scrollableMainSizeOfLines.add(maxScrollableMainSizeOfLine); scrollableCrossSizeOfLines.add(maxScrollableCrossSizeOfLine); @@ -6349,13 +5679,12 @@ class RenderFlexLayout extends RenderLayoutBox { double maxScrollableMainSizeOfLines = scrollableMainSizeOfLines.isEmpty ? 0 : scrollableMainSizeOfLines.reduce((double curr, double next) { - return curr > next ? curr : next; - }); + return curr > next ? curr : next; + }); RenderBoxModel container = this; - bool isScrollContainer = - renderStyle.effectiveOverflowX != CSSOverflowType.visible || - renderStyle.effectiveOverflowY != CSSOverflowType.visible; + bool isScrollContainer = renderStyle.effectiveOverflowX != CSSOverflowType.visible || + renderStyle.effectiveOverflowY != CSSOverflowType.visible; // Child positions already include physical start padding. Only the trailing // padding needs to be added here. @@ -6366,8 +5695,8 @@ class RenderFlexLayout extends RenderLayoutBox { double maxScrollableCrossSizeOfLines = scrollableCrossSizeOfLines.isEmpty ? 0 : scrollableCrossSizeOfLines.reduce((double curr, double next) { - return curr > next ? curr : next; - }); + return curr > next ? curr : next; + }); // Padding in the end direction of axis should be included in scroll container. double maxScrollableCrossSizeOfChildren = maxScrollableCrossSizeOfLines + @@ -6381,15 +5710,9 @@ class RenderFlexLayout extends RenderLayoutBox { container.renderStyle.effectiveBorderTopWidth.computedValue - container.renderStyle.effectiveBorderBottomWidth.computedValue; double maxScrollableMainSize = math.max( - _isHorizontalFlexDirection - ? containerContentWidth - : containerContentHeight, - maxScrollableMainSizeOfChildren); + _isHorizontalFlexDirection ? containerContentWidth : containerContentHeight, maxScrollableMainSizeOfChildren); double maxScrollableCrossSize = math.max( - _isHorizontalFlexDirection - ? containerContentHeight - : containerContentWidth, - maxScrollableCrossSizeOfChildren); + _isHorizontalFlexDirection ? containerContentHeight : containerContentWidth, maxScrollableCrossSizeOfChildren); scrollableSize = _isHorizontalFlexDirection ? Size(maxScrollableMainSize, maxScrollableCrossSize) @@ -6397,13 +5720,10 @@ class RenderFlexLayout extends RenderLayoutBox { } // Get the cross size of flex line based on flex-wrap and align-items/align-self properties. - double _getFlexLineCrossSize( - RenderBox child, - double runCrossAxisExtent, - double runBetweenSpace, - ) { - bool isSingleLine = (renderStyle.flexWrap != FlexWrap.wrap && - renderStyle.flexWrap != FlexWrap.wrapReverse); + double _getFlexLineCrossSize(RenderBox child, + double runCrossAxisExtent, + double runBetweenSpace,) { + bool isSingleLine = (renderStyle.flexWrap != FlexWrap.wrap && renderStyle.flexWrap != FlexWrap.wrapReverse); if (isSingleLine) { final bool hasDefiniteContainerCross = _hasDefiniteContainerCrossSize(); @@ -6414,14 +5734,11 @@ class RenderFlexLayout extends RenderLayoutBox { // flex containers the flex line’s cross size equals the flex container’s inner cross size // when that size is definite. // https://www.w3.org/TR/css-flexbox-1/#algo-cross-line - double? explicitContainerCross; // from explicit non-auto width/height - double? - resolvedContainerCross; // resolved cross size for block-level flex when auto - double? minCrossFromConstraints; // content-box min cross size - double? - minCrossFromStyle; // content-box min cross size derived from min-width/min-height - double? - containerInnerCross; // measured inner cross size from this layout pass + double? explicitContainerCross; // from explicit non-auto width/height + double? resolvedContainerCross; // resolved cross size for block-level flex when auto + double? minCrossFromConstraints; // content-box min cross size + double? minCrossFromStyle; // content-box min cross size derived from min-width/min-height + double? containerInnerCross; // measured inner cross size from this layout pass final CSSDisplay effectiveDisplay = renderStyle.effectiveDisplay; final bool isInlineFlex = effectiveDisplay == CSSDisplay.inlineFlex; final CSSWritingMode wm = renderStyle.writingMode; @@ -6437,33 +5754,27 @@ class RenderFlexLayout extends RenderLayoutBox { // Row: cross axis is height. final double maxH = constraints.maxHeight; if (maxH.isFinite) { - final double borderV = - renderStyle.effectiveBorderTopWidth.computedValue + - renderStyle.effectiveBorderBottomWidth.computedValue; - final double paddingV = renderStyle.paddingTop.computedValue + - renderStyle.paddingBottom.computedValue; + final double borderV = renderStyle.effectiveBorderTopWidth.computedValue + + renderStyle.effectiveBorderBottomWidth.computedValue; + final double paddingV = renderStyle.paddingTop.computedValue + renderStyle.paddingBottom.computedValue; availableInnerCross = math.max(0, maxH - borderV - paddingV); } } else { // Column: cross axis is width. final double maxW = constraints.maxWidth; if (maxW.isFinite) { - final double borderH = - renderStyle.effectiveBorderLeftWidth.computedValue + - renderStyle.effectiveBorderRightWidth.computedValue; - final double paddingH = renderStyle.paddingLeft.computedValue + - renderStyle.paddingRight.computedValue; + final double borderH = renderStyle.effectiveBorderLeftWidth.computedValue + + renderStyle.effectiveBorderRightWidth.computedValue; + final double paddingH = renderStyle.paddingLeft.computedValue + renderStyle.paddingRight.computedValue; availableInnerCross = math.max(0, maxW - borderH - paddingH); } } } double clampToAvailable(double value) { - if (availableInnerCross == null || !availableInnerCross.isFinite) - return value; + if (availableInnerCross == null || !availableInnerCross.isFinite) return value; return value > availableInnerCross ? availableInnerCross : value; } - if (_isHorizontalFlexDirection) { // Row: cross is height // Only treat as definite if height is explicitly specified (not auto) @@ -6471,9 +5782,7 @@ class RenderFlexLayout extends RenderLayoutBox { explicitContainerCross = renderStyle.contentBoxLogicalHeight; } // Also consider the actually measured inner cross size from this layout pass. - if (hasDefiniteContainerCross && - contentSize.height.isFinite && - contentSize.height > 0) { + if (hasDefiniteContainerCross && contentSize.height.isFinite && contentSize.height > 0) { containerInnerCross = contentSize.height; } // min-height should also participate in establishing the line cross size @@ -6481,17 +5790,14 @@ class RenderFlexLayout extends RenderLayoutBox { // is otherwise indefinite (e.g. height:auto under grid layout constraints). if (renderStyle.minHeight.isNotAuto) { final double minBorderBox = renderStyle.minHeight.computedValue; - double minContentBox = - renderStyle.deflatePaddingBorderHeight(minBorderBox); + double minContentBox = renderStyle.deflatePaddingBorderHeight(minBorderBox); if (minContentBox.isFinite && minContentBox < 0) minContentBox = 0; if (minContentBox.isFinite && minContentBox > 0) { minCrossFromStyle = minContentBox; } } // Height:auto is generally not definite prior to layout; still capture a min-cross constraint if present. - if (contentConstraints != null && - contentConstraints!.minHeight.isFinite && - contentConstraints!.minHeight > 0) { + if (contentConstraints != null && contentConstraints!.minHeight.isFinite && contentConstraints!.minHeight > 0) { minCrossFromConstraints = contentConstraints!.minHeight; } } else { @@ -6502,13 +5808,9 @@ class RenderFlexLayout extends RenderLayoutBox { } // For block-level flex with width:auto in horizontal writing mode, the used width // is fill-available and thus definite; only then may we resolve from constraints. - if (hasDefiniteContainerCross && - !isInlineFlex && - (explicitContainerCross == null) && - crossIsWidth && + if (hasDefiniteContainerCross && !isInlineFlex && (explicitContainerCross == null) && crossIsWidth && wm == CSSWritingMode.horizontalTb) { - if (contentConstraints != null && - contentConstraints!.hasBoundedWidth && + if (contentConstraints != null && contentConstraints!.hasBoundedWidth && contentConstraints!.maxWidth.isFinite) { resolvedContainerCross = contentConstraints!.maxWidth; } @@ -6517,34 +5819,26 @@ class RenderFlexLayout extends RenderLayoutBox { // content width as a definite line cross size; width:auto should shrink-to-fit // in vertical writing modes. Only consider the measured inner width when the // container has an explicit (non-auto) width. - if (hasDefiniteContainerCross && - renderStyle.width.isNotAuto && - contentSize.width.isFinite && - contentSize.width > 0) { + if (hasDefiniteContainerCross && renderStyle.width.isNotAuto && contentSize.width.isFinite && contentSize.width > 0) { containerInnerCross = contentSize.width; } if (renderStyle.minWidth.isNotAuto) { final double minBorderBox = renderStyle.minWidth.computedValue; - double minContentBox = - renderStyle.deflatePaddingBorderWidth(minBorderBox); + double minContentBox = renderStyle.deflatePaddingBorderWidth(minBorderBox); if (minContentBox.isFinite && minContentBox < 0) minContentBox = 0; if (minContentBox.isFinite && minContentBox > 0) { minCrossFromStyle = minContentBox; } } - if (contentConstraints != null && - contentConstraints!.minWidth.isFinite && - contentConstraints!.minWidth > 0) { + if (contentConstraints != null && contentConstraints!.minWidth.isFinite && contentConstraints!.minWidth > 0) { minCrossFromConstraints = contentConstraints!.minWidth; } } // Prefer the larger of the style-derived and constraints-derived minimum cross sizes. if (minCrossFromStyle != null && minCrossFromStyle!.isFinite) { - if (minCrossFromConstraints != null && - minCrossFromConstraints!.isFinite) { - minCrossFromConstraints = - math.max(minCrossFromConstraints!, minCrossFromStyle!); + if (minCrossFromConstraints != null && minCrossFromConstraints!.isFinite) { + minCrossFromConstraints = math.max(minCrossFromConstraints!, minCrossFromStyle!); } else { minCrossFromConstraints = minCrossFromStyle; } @@ -6569,9 +5863,7 @@ class RenderFlexLayout extends RenderLayoutBox { if (containerInnerCross != null && containerInnerCross.isFinite) { return containerInnerCross; } - if (!isInlineFlex && - resolvedContainerCross != null && - resolvedContainerCross.isFinite) { + if (!isInlineFlex && resolvedContainerCross != null && resolvedContainerCross.isFinite) { return resolvedContainerCross; } } @@ -6582,8 +5874,7 @@ class RenderFlexLayout extends RenderLayoutBox { return runCrossAxisExtent; } else { // Flex line of align-content stretch should includes between space. - bool isMultiLineStretch = - renderStyle.alignContent == AlignContent.stretch; + bool isMultiLineStretch = renderStyle.alignContent == AlignContent.stretch; if (isMultiLineStretch) { return runCrossAxisExtent + runBetweenSpace; } else { @@ -6593,9 +5884,7 @@ class RenderFlexLayout extends RenderLayoutBox { } // Set children offset based on alignment properties. - void _setChildrenOffset( - List<_RunMetrics> runMetrics, - ) { + void _setChildrenOffset(List<_RunMetrics> runMetrics,) { if (runMetrics.isEmpty) return; final bool isHorizontal = _isHorizontalFlexDirection; @@ -6634,23 +5923,19 @@ class RenderFlexLayout extends RenderLayoutBox { // intrinsic contentSize to the actual inner box size from Flutter // constraints so that alignment (justify-content/align-items) operates // within the visible box instead of an unconstrained CSS height/width. - final double borderLeft = - renderStyle.effectiveBorderLeftWidth.computedValue; - final double borderRight = - renderStyle.effectiveBorderRightWidth.computedValue; - final double borderTop = - renderStyle.effectiveBorderTopWidth.computedValue; - final double borderBottom = - renderStyle.effectiveBorderBottomWidth.computedValue; + final double borderLeft = renderStyle.effectiveBorderLeftWidth.computedValue; + final double borderRight = renderStyle.effectiveBorderRightWidth.computedValue; + final double borderTop = renderStyle.effectiveBorderTopWidth.computedValue; + final double borderBottom = renderStyle.effectiveBorderBottomWidth.computedValue; final double paddingLeft = renderStyle.paddingLeft.computedValue; final double paddingRight = renderStyle.paddingRight.computedValue; final double paddingTop = renderStyle.paddingTop.computedValue; final double paddingBottom = renderStyle.paddingBottom.computedValue; - final double innerWidthFromSize = math.max(0.0, - size.width - borderLeft - borderRight - paddingLeft - paddingRight); - final double innerHeightFromSize = math.max(0.0, - size.height - borderTop - borderBottom - paddingTop - paddingBottom); + final double innerWidthFromSize = + math.max(0.0, size.width - borderLeft - borderRight - paddingLeft - paddingRight); + final double innerHeightFromSize = + math.max(0.0, size.height - borderTop - borderBottom - paddingTop - paddingBottom); double contentMainExtent; double contentCrossExtent; @@ -6676,8 +5961,7 @@ class RenderFlexLayout extends RenderLayoutBox { if (!mainAxisContentSize.isFinite || mainAxisContentSize <= 0) { mainAxisContentSize = innerMainFromSize; } else { - mainAxisContentSize = - math.min(mainAxisContentSize, innerMainFromSize); + mainAxisContentSize = math.min(mainAxisContentSize, innerMainFromSize); } } crossAxisContentSize = contentCrossExtent; @@ -6685,8 +5969,7 @@ class RenderFlexLayout extends RenderLayoutBox { if (!crossAxisContentSize.isFinite || crossAxisContentSize <= 0) { crossAxisContentSize = innerCrossFromSize; } else { - crossAxisContentSize = - math.min(crossAxisContentSize, innerCrossFromSize); + crossAxisContentSize = math.min(crossAxisContentSize, innerCrossFromSize); } } } else { @@ -6737,9 +6020,7 @@ class RenderFlexLayout extends RenderLayoutBox { if (remainingSpace < 0) { betweenSpace = 0.0; } else { - betweenSpace = runChildrenCount > 1 - ? remainingSpace / (runChildrenCount - 1) - : 0.0; + betweenSpace = runChildrenCount > 1 ? remainingSpace / (runChildrenCount - 1) : 0.0; } break; case JustifyContent.spaceAround: @@ -6747,8 +6028,7 @@ class RenderFlexLayout extends RenderLayoutBox { leadingSpace = remainingSpace / 2.0; betweenSpace = 0.0; } else { - betweenSpace = - runChildrenCount > 0 ? remainingSpace / runChildrenCount : 0.0; + betweenSpace = runChildrenCount > 0 ? remainingSpace / runChildrenCount : 0.0; leadingSpace = betweenSpace / 2.0; } break; @@ -6757,9 +6037,7 @@ class RenderFlexLayout extends RenderLayoutBox { leadingSpace = remainingSpace / 2.0; betweenSpace = 0.0; } else { - betweenSpace = runChildrenCount > 0 - ? remainingSpace / (runChildrenCount + 1) - : 0.0; + betweenSpace = runChildrenCount > 0 ? remainingSpace / (runChildrenCount + 1) : 0.0; leadingSpace = betweenSpace; } break; @@ -6785,23 +6063,17 @@ class RenderFlexLayout extends RenderLayoutBox { // Main axis position of child on layout. double childMainPosition = flipMainAxis - ? mainAxisStartPadding + - mainAxisStartBorder + - mainAxisContentSize - - leadingSpace + ? mainAxisStartPadding + mainAxisStartBorder + mainAxisContentSize - leadingSpace : leadingSpace + mainAxisStartPadding + mainAxisStartBorder; - final bool runAllChildrenAtMaxCross = _areAllRunChildrenAtMaxCrossExtent( - runChildrenList, runCrossAxisExtent); + final bool runAllChildrenAtMaxCross = _areAllRunChildrenAtMaxCrossExtent(runChildrenList, runCrossAxisExtent); // Per-auto-margin main-axis share. Auto margins in the main axis absorb free space. // https://www.w3.org/TR/css-flexbox-1/#auto-margins - final double perMainAxisAutoMargin = mainAxisAutoMarginCount == 0 - ? 0.0 - : (math.max(0, remainingSpace) / mainAxisAutoMarginCount); + final double perMainAxisAutoMargin = + mainAxisAutoMarginCount == 0 ? 0.0 : (math.max(0, remainingSpace) / mainAxisAutoMarginCount); - final bool mainAxisStartAtPhysicalStart = - _isMainAxisStartAtPhysicalStart(); + final bool mainAxisStartAtPhysicalStart = _isMainAxisStartAtPhysicalStart(); for (_RunChild runChild in runChildrenList) { RenderBox child = runChild.child; @@ -6813,36 +6085,23 @@ class RenderFlexLayout extends RenderLayoutBox { // Position the child along the main axis respecting direction. final bool mainAxisStartAuto = isHorizontal - ? (mainAxisStartAtPhysicalStart - ? runChild.marginLeftAuto - : runChild.marginRightAuto) - : (mainAxisStartAtPhysicalStart - ? runChild.marginTopAuto - : runChild.marginBottomAuto); + ? (mainAxisStartAtPhysicalStart ? runChild.marginLeftAuto : runChild.marginRightAuto) + : (mainAxisStartAtPhysicalStart ? runChild.marginTopAuto : runChild.marginBottomAuto); final bool mainAxisEndAuto = isHorizontal - ? (mainAxisStartAtPhysicalStart - ? runChild.marginRightAuto - : runChild.marginLeftAuto) - : (mainAxisStartAtPhysicalStart - ? runChild.marginBottomAuto - : runChild.marginTopAuto); - final double startAutoSpace = - mainAxisStartAuto ? perMainAxisAutoMargin : 0.0; - final double endAutoSpace = - mainAxisEndAuto ? perMainAxisAutoMargin : 0.0; + ? (mainAxisStartAtPhysicalStart ? runChild.marginRightAuto : runChild.marginLeftAuto) + : (mainAxisStartAtPhysicalStart ? runChild.marginBottomAuto : runChild.marginTopAuto); + final double startAutoSpace = mainAxisStartAuto ? perMainAxisAutoMargin : 0.0; + final double endAutoSpace = mainAxisEndAuto ? perMainAxisAutoMargin : 0.0; if (flipMainAxis) { // In reversed main axis (e.g., column-reverse or RTL row), advance from the // far edge by the start margin and the child's own size. Do not subtract the // trailing (end) margin here — it separates this item from the next. - final double adjStartMargin = - _calculateMainAxisMarginForJustContentType(childStartMargin); - childMainPosition -= - (startAutoSpace + adjStartMargin + childMainSizeOnly); + final double adjStartMargin = _calculateMainAxisMarginForJustContentType(childStartMargin); + childMainPosition -= (startAutoSpace + adjStartMargin + childMainSizeOnly); } else { // In normal flow, advance by the start margin before placing. - childMainPosition += - _calculateMainAxisMarginForJustContentType(childStartMargin); + childMainPosition += _calculateMainAxisMarginForJustContentType(childStartMargin); childMainPosition += startAutoSpace; } double? childCrossPosition; @@ -6854,13 +6113,11 @@ class RenderFlexLayout extends RenderLayoutBox { case AlignSelf.flexStart: case AlignSelf.start: case AlignSelf.stretch: - alignment = - renderStyle.flexWrap == FlexWrap.wrapReverse ? 'end' : 'start'; + alignment = renderStyle.flexWrap == FlexWrap.wrapReverse ? 'end' : 'start'; break; case AlignSelf.flexEnd: case AlignSelf.end: - alignment = - renderStyle.flexWrap == FlexWrap.wrapReverse ? 'start' : 'end'; + alignment = renderStyle.flexWrap == FlexWrap.wrapReverse ? 'start' : 'end'; break; case AlignSelf.center: alignment = 'center'; @@ -6874,22 +6131,18 @@ class RenderFlexLayout extends RenderLayoutBox { case AlignItems.flexStart: case AlignItems.start: case AlignItems.stretch: - alignment = renderStyle.flexWrap == FlexWrap.wrapReverse - ? 'end' - : 'start'; + alignment = renderStyle.flexWrap == FlexWrap.wrapReverse ? 'end' : 'start'; break; case AlignItems.flexEnd: case AlignItems.end: - alignment = renderStyle.flexWrap == FlexWrap.wrapReverse - ? 'start' - : 'end'; + alignment = renderStyle.flexWrap == FlexWrap.wrapReverse ? 'start' : 'end'; break; case AlignItems.center: alignment = 'center'; break; case AlignItems.baseline: case AlignItems.lastBaseline: - // FIXME: baseline alignment in wrap-reverse flexWrap may display different from browser in some case + // FIXME: baseline alignment in wrap-reverse flexWrap may display different from browser in some case if (isHorizontal) { alignment = 'baseline'; } else if (renderStyle.flexWrap == FlexWrap.wrapReverse) { @@ -6908,9 +6161,7 @@ class RenderFlexLayout extends RenderLayoutBox { // Text is aligned in anonymous block container rather than flexbox container. // https://www.w3.org/TR/css-flexbox-1/#flex-items - if (renderStyle.alignItems == AlignItems.stretch && - child is RenderTextBox && - !isHorizontal) { + if (renderStyle.alignItems == AlignItems.stretch && child is RenderTextBox && !isHorizontal) { TextAlign textAlign = renderStyle.textAlign; if (textAlign == TextAlign.start) { alignment = 'start'; @@ -6942,29 +6193,24 @@ class RenderFlexLayout extends RenderLayoutBox { // https://www.w3.org/TR/css-flexbox-1/#auto-margins if (child is RenderBoxModel) { // Margin auto does not work with negative remaining space. - final double crossAxisRemainingSpace = - math.max(0, crossAxisContentSize - childCrossAxisExtent); + final double crossAxisRemainingSpace = math.max(0, crossAxisContentSize - childCrossAxisExtent); if (isHorizontal) { // Cross axis is vertical (top/bottom). if (runChild.marginTopAuto) { if (runChild.marginBottomAuto) { - childCrossPosition = - childCrossPosition! + crossAxisRemainingSpace / 2; + childCrossPosition = childCrossPosition! + crossAxisRemainingSpace / 2; } else { - childCrossPosition = - childCrossPosition! + crossAxisRemainingSpace; + childCrossPosition = childCrossPosition! + crossAxisRemainingSpace; } } } else { // Cross axis is horizontal (left/right). if (runChild.marginLeftAuto) { if (runChild.marginRightAuto) { - childCrossPosition = - childCrossPosition! + crossAxisRemainingSpace / 2; + childCrossPosition = childCrossPosition! + crossAxisRemainingSpace / 2; } else { - childCrossPosition = - childCrossPosition! + crossAxisRemainingSpace; + childCrossPosition = childCrossPosition! + crossAxisRemainingSpace; } } } @@ -6974,11 +6220,8 @@ class RenderFlexLayout extends RenderLayoutBox { double crossOffset; if (renderStyle.flexWrap == FlexWrap.wrapReverse) { - crossOffset = childCrossPosition! + - (crossAxisContentSize - - crossAxisOffset - - runCrossAxisExtent - - runBetweenSpace); + crossOffset = + childCrossPosition! + (crossAxisContentSize - crossAxisOffset - runCrossAxisExtent - runBetweenSpace); } else { crossOffset = childCrossPosition! + crossAxisOffset; } @@ -6995,15 +6238,10 @@ class RenderFlexLayout extends RenderLayoutBox { if (flipMainAxis) { // After placing in reversed flow, move past the trailing (end) margin, // then account for between-space and gaps. - childMainPosition -= - (childEndMargin + endAutoSpace + betweenSpace + effectiveGap); + childMainPosition -= (childEndMargin + endAutoSpace + betweenSpace + effectiveGap); } else { // Normal flow: advance by the child size, trailing margin, between-space and gaps. - childMainPosition += (childMainSizeOnly + - childEndMargin + - endAutoSpace + - betweenSpace + - effectiveGap); + childMainPosition += (childMainSizeOnly + childEndMargin + endAutoSpace + betweenSpace + effectiveGap); } } @@ -7066,16 +6304,14 @@ class RenderFlexLayout extends RenderLayoutBox { // Replaced elements (e.g., ) with an intrinsic aspect ratio should not be // stretched in the cross axis; browsers keep their border-box proportional even // under align-items: stretch. This matches CSS Flexbox §9.4. - if (_shouldPreserveIntrinsicRatio(childBoxModel, - hasDefiniteContainerCross: hasDefiniteCrossSize)) { + if (_shouldPreserveIntrinsicRatio(childBoxModel, hasDefiniteContainerCross: hasDefiniteCrossSize)) { return false; } return true; } - bool _shouldPreserveIntrinsicRatio(RenderBoxModel child, - {required bool hasDefiniteContainerCross}) { + bool _shouldPreserveIntrinsicRatio(RenderBoxModel child, {required bool hasDefiniteContainerCross}) { if (child is! RenderReplaced) { return false; } @@ -7094,78 +6330,57 @@ class RenderFlexLayout extends RenderLayoutBox { bool _hasDefiniteContainerCrossSize() { if (_isHorizontalFlexDirection) { if (renderStyle.contentBoxLogicalHeight != null) return true; - if (contentConstraints != null && contentConstraints!.hasTightHeight) - return true; + if (contentConstraints != null && contentConstraints!.hasTightHeight) return true; if (constraints.hasTightHeight) return true; return false; } else { if (renderStyle.contentBoxLogicalWidth != null) return true; - if (contentConstraints != null && contentConstraints!.hasTightWidth) - return true; + if (contentConstraints != null && contentConstraints!.hasTightWidth) return true; if (constraints.hasTightWidth) return true; return false; } } // Get child stretched size in the cross axis. - double _getChildStretchedCrossSize( - RenderBoxModel child, - double runCrossAxisExtent, - double runBetweenSpace, - ) { - bool isFlexWrap = renderStyle.flexWrap == FlexWrap.wrap || - renderStyle.flexWrap == FlexWrap.wrapReverse; - double childCrossAxisMargin = _horizontalMarginNegativeSet(0, child, - isHorizontal: !_isHorizontalFlexDirection); - _isHorizontalFlexDirection - ? child.renderStyle.margin.vertical - : child.renderStyle.margin.horizontal; - double maxCrossSizeConstraints = _isHorizontalFlexDirection - ? constraints.maxHeight - : constraints.maxWidth; - double flexLineCrossSize = - _getFlexLineCrossSize(child, runCrossAxisExtent, runBetweenSpace); + double _getChildStretchedCrossSize(RenderBoxModel child, + double runCrossAxisExtent, + double runBetweenSpace,) { + bool isFlexWrap = renderStyle.flexWrap == FlexWrap.wrap || renderStyle.flexWrap == FlexWrap.wrapReverse; + double childCrossAxisMargin = _horizontalMarginNegativeSet(0, child, isHorizontal: !_isHorizontalFlexDirection); + _isHorizontalFlexDirection ? child.renderStyle.margin.vertical : child.renderStyle.margin.horizontal; + double maxCrossSizeConstraints = _isHorizontalFlexDirection ? constraints.maxHeight : constraints.maxWidth; + double flexLineCrossSize = _getFlexLineCrossSize(child, runCrossAxisExtent, runBetweenSpace); // Should subtract margin when stretch flex item. double childStretchedCrossSize = flexLineCrossSize - childCrossAxisMargin; // Flex line cross size should not exceed container's cross size if specified when flex-wrap is nowrap. if (!isFlexWrap && maxCrossSizeConstraints.isFinite) { - double crossAxisBorder = _isHorizontalFlexDirection - ? renderStyle.border.vertical - : renderStyle.border.horizontal; - double crossAxisPadding = _isHorizontalFlexDirection - ? renderStyle.padding.vertical - : renderStyle.padding.horizontal; - childStretchedCrossSize = math.min( - maxCrossSizeConstraints - crossAxisBorder - crossAxisPadding, - childStretchedCrossSize); + double crossAxisBorder = _isHorizontalFlexDirection ? renderStyle.border.vertical : renderStyle.border.horizontal; + double crossAxisPadding = + _isHorizontalFlexDirection ? renderStyle.padding.vertical : renderStyle.padding.horizontal; + childStretchedCrossSize = + math.min(maxCrossSizeConstraints - crossAxisBorder - crossAxisPadding, childStretchedCrossSize); } // Constrain stretched size by max-width/max-height. double? maxCrossSize; if (_isHorizontalFlexDirection && child.renderStyle.maxHeight.isNotNone) { maxCrossSize = child.renderStyle.maxHeight.computedValue; - } else if (!_isHorizontalFlexDirection && - child.renderStyle.maxWidth.isNotNone) { + } else if (!_isHorizontalFlexDirection && child.renderStyle.maxWidth.isNotNone) { maxCrossSize = child.renderStyle.maxWidth.computedValue; } if (maxCrossSize != null) { - childStretchedCrossSize = childStretchedCrossSize > maxCrossSize - ? maxCrossSize - : childStretchedCrossSize; + childStretchedCrossSize = childStretchedCrossSize > maxCrossSize ? maxCrossSize : childStretchedCrossSize; } // Constrain stretched size by min-width/min-height. double? minCrossSize; if (_isHorizontalFlexDirection && child.renderStyle.minHeight.isNotAuto) { minCrossSize = child.renderStyle.minHeight.computedValue; - } else if (!_isHorizontalFlexDirection && - child.renderStyle.minWidth.isNotAuto) { + } else if (!_isHorizontalFlexDirection && child.renderStyle.minWidth.isNotAuto) { minCrossSize = child.renderStyle.minWidth.computedValue; } if (minCrossSize != null) { - childStretchedCrossSize = childStretchedCrossSize < minCrossSize - ? minCrossSize - : childStretchedCrossSize; + childStretchedCrossSize = childStretchedCrossSize < minCrossSize ? minCrossSize : childStretchedCrossSize; } // Ensure stretched height in row-direction is not smaller than the @@ -7203,30 +6418,27 @@ class RenderFlexLayout extends RenderLayoutBox { final CSSLengthValue marginRight = s.marginRight; final CSSLengthValue marginTop = s.marginTop; final CSSLengthValue marginBottom = s.marginBottom; - if (_isHorizontalFlexDirection && - (marginTop.isAuto || marginBottom.isAuto)) return true; - if (!_isHorizontalFlexDirection && - (marginLeft.isAuto || marginRight.isAuto)) return true; + if (_isHorizontalFlexDirection && (marginTop.isAuto || marginBottom.isAuto)) return true; + if (!_isHorizontalFlexDirection && (marginLeft.isAuto || marginRight.isAuto)) return true; } return false; } // Get flex item cross axis offset by align-items/align-self. - double? _getChildCrossAxisOffset( - String alignment, - RenderBox child, - double? childCrossPosition, - double runBaselineExtent, - double runCrossAxisExtent, - double runBetweenSpace, - double crossAxisStartPadding, - double crossAxisStartBorder, { - required double childCrossAxisExtent, - required double childCrossAxisStartMargin, - required double childCrossAxisEndMargin, - required bool hasAutoCrossAxisMargin, - required bool runAllChildrenAtMaxCross, - }) { + double? _getChildCrossAxisOffset(String alignment, + RenderBox child, + double? childCrossPosition, + double runBaselineExtent, + double runCrossAxisExtent, + double runBetweenSpace, + double crossAxisStartPadding, + double crossAxisStartBorder, { + required double childCrossAxisExtent, + required double childCrossAxisStartMargin, + required double childCrossAxisEndMargin, + required bool hasAutoCrossAxisMargin, + required bool runAllChildrenAtMaxCross, + }) { // Leading between height of line box's content area and line height of line box. double lineBoxLeading = 0; double? lineBoxHeight = _getLineHeight(this); @@ -7240,16 +6452,13 @@ class RenderFlexLayout extends RenderLayoutBox { runBetweenSpace, ); // start offset including margin (used by start/end alignment) - double crossStartAddedOffset = crossAxisStartPadding + - crossAxisStartBorder + - childCrossAxisStartMargin; + double crossStartAddedOffset = crossAxisStartPadding + crossAxisStartBorder + childCrossAxisStartMargin; // start offset without margin (used by center alignment where we center the margin-box itself) double crossStartNoMargin = crossAxisStartPadding + crossAxisStartBorder; final _FlexContainerInvariants? inv = _layoutInvariants; final bool crossIsHorizontal; - final bool - crossStartIsPhysicalStart; // left for horizontal, top for vertical + final bool crossStartIsPhysicalStart; // left for horizontal, top for vertical if (inv != null) { crossIsHorizontal = inv.isCrossAxisHorizontal; crossStartIsPhysicalStart = inv.isCrossAxisStartAtPhysicalStart; @@ -7257,8 +6466,7 @@ class RenderFlexLayout extends RenderLayoutBox { // Determine cross axis orientation and where cross-start maps physically. final CSSWritingMode wm = renderStyle.writingMode; final bool inlineIsHorizontal = (wm == CSSWritingMode.horizontalTb); - if (renderStyle.flexDirection == FlexDirection.row || - renderStyle.flexDirection == FlexDirection.rowReverse) { + if (renderStyle.flexDirection == FlexDirection.row || renderStyle.flexDirection == FlexDirection.rowReverse) { // Cross is block axis. crossIsHorizontal = !inlineIsHorizontal; if (crossIsHorizontal) { @@ -7273,8 +6481,7 @@ class RenderFlexLayout extends RenderLayoutBox { crossIsHorizontal = inlineIsHorizontal; if (crossIsHorizontal) { // Inline-start follows text direction in horizontal-tb. - crossStartIsPhysicalStart = - (renderStyle.direction != TextDirection.rtl); + crossStartIsPhysicalStart = (renderStyle.direction != TextDirection.rtl); } else { // Inline-start is physical top in vertical writing modes. crossStartIsPhysicalStart = true; @@ -7309,20 +6516,16 @@ class RenderFlexLayout extends RenderLayoutBox { childCrossAxisStartMargin; } else { // Cross-start at right: cross-end is left (physical start) - return crossAxisStartPadding + - crossAxisStartBorder + - childCrossAxisEndMargin; + return crossAxisStartPadding + crossAxisStartBorder + childCrossAxisEndMargin; } case 'center': // Center the child's MARGIN-BOX within the flex line's content box (spec behavior). // We first get the child's cross-extent including margins, then derive the // border-box extent for overflow heuristics and logging. - final double childExtentWithMargin = - childCrossAxisExtent; // includes margins + final double childExtentWithMargin = childCrossAxisExtent; // includes margins final double startMargin = childCrossAxisStartMargin; final double endMargin = childCrossAxisEndMargin; - final double borderBoxExtent = - math.max(0.0, childExtentWithMargin - (startMargin + endMargin)); + final double borderBoxExtent = math.max(0.0, childExtentWithMargin - (startMargin + endMargin)); // Center within the content box by default (spec-aligned). // Additionally, for vertical cross-axes (row direction), when the item overflows // the content box (free space < 0) and the container has cross-axis padding, @@ -7340,38 +6543,26 @@ class RenderFlexLayout extends RenderLayoutBox { // centering of padded controls. This preserves expected behavior for // headers/toolbars where padding defines visual bounds. final double padStart = crossAxisStartPadding; - final double padEnd = inv?.crossAxisPaddingEnd ?? - _flowAwareCrossAxisPadding(isEnd: true); + final double padEnd = inv?.crossAxisPaddingEnd ?? _flowAwareCrossAxisPadding(isEnd: true); final double borderStart = crossAxisStartBorder; - final double borderEnd = inv?.crossAxisBorderEnd ?? - renderStyle.effectiveBorderBottomWidth.computedValue; - final double padBorderSum = - padStart + padEnd + borderStart + borderEnd; + final double borderEnd = inv?.crossAxisBorderEnd ?? renderStyle.effectiveBorderBottomWidth.computedValue; + final double padBorderSum = padStart + padEnd + borderStart + borderEnd; final double freeSpace = flexLineCrossSize - borderBoxExtent; const double kEpsilon = 0.0001; final bool isExactFit = freeSpace.abs() <= kEpsilon; - if (padBorderSum > 0 && - (freeSpace < 0 || (isExactFit && runAllChildrenAtMaxCross))) { + if (padBorderSum > 0 && (freeSpace < 0 || (isExactFit && runAllChildrenAtMaxCross))) { // Determine container content cross size (definite if set on style), // fall back to the current line cross size. - final double containerContentCross = - renderStyle.contentBoxLogicalHeight ?? flexLineCrossSize; - final double containerBorderCross = containerContentCross + - padStart + - padEnd + - borderStart + - borderEnd; - final double posFromBorder = - (containerBorderCross - borderBoxExtent) / 2.0; - final double pos = - posFromBorder; // since offsets are measured from border-start + final double containerContentCross = renderStyle.contentBoxLogicalHeight ?? flexLineCrossSize; + final double containerBorderCross = containerContentCross + padStart + padEnd + borderStart + borderEnd; + final double posFromBorder = (containerBorderCross - borderBoxExtent) / 2.0; + final double pos = posFromBorder; // since offsets are measured from border-start return pos.isFinite ? pos : crossStartNoMargin; } } // If the margin-box is equal to or wider than the line cross size, pin to start // to avoid introducing cross-axis offset that would create horizontal scroll. - final double marginBoxExtent = - borderBoxExtent + startMargin + endMargin; + final double marginBoxExtent = borderBoxExtent + startMargin + endMargin; // Only clamp for horizontal cross-axis (i.e., when cross is width), and only when // overflow is caused by margins (border-box fits, margin-box overflows). If the // border-box itself is wider than the line, we still center (allow negative offset) @@ -7379,33 +6570,28 @@ class RenderFlexLayout extends RenderLayoutBox { // Only treat as overflow when the margin-box actually exceeds the line cross size. // If it exactly equals, there is no overflow and centering should place the // border-box at startMargin (i.e., symmetric gaps), matching browser behavior. - final bool marginOnlyOverflow = borderBoxExtent <= flexLineCrossSize && - marginBoxExtent > flexLineCrossSize; + final bool marginOnlyOverflow = borderBoxExtent <= flexLineCrossSize && marginBoxExtent > flexLineCrossSize; if (crossIsHorizontal && marginOnlyOverflow) { return crossStartNoMargin; } // Center the margin-box in the line's content box, then add the start margin // to obtain the border-box offset. final double freeInContent = flexLineCrossSize - marginBoxExtent; - final double pos = - crossStartNoMargin + freeInContent / 2.0 + startMargin; + final double pos = crossStartNoMargin + freeInContent / 2.0 + startMargin; return pos; case 'baseline': - // In column flex-direction (vertical main axis), baseline alignment behaves - // like flex-start per our layout model. Avoid using runBaselineExtent which - // is not computed for vertical-main containers. + // In column flex-direction (vertical main axis), baseline alignment behaves + // like flex-start per our layout model. Avoid using runBaselineExtent which + // is not computed for vertical-main containers. if (!_isHorizontalFlexDirection) { return crossStartAddedOffset; } // Distance from top to baseline of child. double childAscent = _getChildAscent(child); - final double offset = crossStartAddedOffset + - lineBoxLeading / 2 + - (runBaselineExtent - childAscent); + final double offset = crossStartAddedOffset + lineBoxLeading / 2 + (runBaselineExtent - childAscent); if (DebugFlags.debugLogFlexBaselineEnabled) { // ignore: avoid_print - print( - '[FlexBaseline] offset child=${child.runtimeType}#${child.hashCode} ' + print('[FlexBaseline] offset child=${child.runtimeType}#${child.hashCode} ' 'runBaseline=${runBaselineExtent.toStringAsFixed(2)} ' 'childAscent=${childAscent.toStringAsFixed(2)} ' 'lineLeading=${lineBoxLeading.toStringAsFixed(2)} ' @@ -7418,8 +6604,7 @@ class RenderFlexLayout extends RenderLayoutBox { } } - bool _areAllRunChildrenAtMaxCrossExtent( - List<_RunChild> runChildren, double runCrossAxisExtent) { + bool _areAllRunChildrenAtMaxCrossExtent(List<_RunChild> runChildren, double runCrossAxisExtent) { const double kEpsilon = 0.0001; for (final _RunChild runChild in runChildren) { final double childCrossAxisExtent = _getCrossAxisExtent(runChild.child); @@ -7437,8 +6622,7 @@ class RenderFlexLayout extends RenderLayoutBox { } // Get child size through boxSize to avoid flutter error when parentUsesSize is set to false. - Size? _getChildSize(RenderBox? child, - {bool shouldUseIntrinsicMainSize = false}) { + Size? _getChildSize(RenderBox? child, {bool shouldUseIntrinsicMainSize = false}) { Size? childSize; if (child != null) { childSize = _transientChildSizeOverrides?[child]; @@ -7559,14 +6743,10 @@ class RenderFlexLayout extends RenderLayoutBox { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(DiagnosticsProperty( - 'flexDirection', renderStyle.flexDirection)); - properties.add(DiagnosticsProperty( - 'justifyContent', renderStyle.justifyContent)); - properties.add( - DiagnosticsProperty('alignItems', renderStyle.alignItems)); - properties - .add(DiagnosticsProperty('flexWrap', renderStyle.flexWrap)); + properties.add(DiagnosticsProperty('flexDirection', renderStyle.flexDirection)); + properties.add(DiagnosticsProperty('justifyContent', renderStyle.justifyContent)); + properties.add(DiagnosticsProperty('alignItems', renderStyle.alignItems)); + properties.add(DiagnosticsProperty('flexWrap', renderStyle.flexWrap)); } static bool _isPlaceholderPositioned(RenderObject child) { From 5542753e258834f82a1259ad731d7981c24ca5c0 Mon Sep 17 00:00:00 2001 From: andycall Date: Fri, 27 Mar 2026 02:26:31 -0700 Subject: [PATCH 05/13] perf(webf): reduce redundant flex fast-path relayouts --- webf/lib/src/foundation/debug_flags.dart | 6 + webf/lib/src/rendering/flex.dart | 334 ++++++++++++++++++++++- 2 files changed, 335 insertions(+), 5 deletions(-) diff --git a/webf/lib/src/foundation/debug_flags.dart b/webf/lib/src/foundation/debug_flags.dart index 7857ce2c15..280b893ec3 100644 --- a/webf/lib/src/foundation/debug_flags.dart +++ b/webf/lib/src/foundation/debug_flags.dart @@ -123,6 +123,12 @@ class DebugFlags { const int.fromEnvironment('WEBF_DEBUG_FLEX_FAST_PATH_SUMMARY_EVERY', defaultValue: 50); static int flexFastPathProfilingMaxDetailLogs = const int.fromEnvironment('WEBF_DEBUG_FLEX_FAST_PATH_MAX_DETAIL_LOGS', defaultValue: 20); + static bool enableFlexAdjustFastPathProfiling = + const bool.fromEnvironment('WEBF_DEBUG_FLEX_ADJUST_FAST_PATH', defaultValue: false); + static int flexAdjustFastPathProfilingSummaryEvery = + const int.fromEnvironment('WEBF_DEBUG_FLEX_ADJUST_FAST_PATH_SUMMARY_EVERY', defaultValue: 50); + static int flexAdjustFastPathProfilingMaxDetailLogs = + const int.fromEnvironment('WEBF_DEBUG_FLEX_ADJUST_FAST_PATH_MAX_DETAIL_LOGS', defaultValue: 20); static bool enableFlexAnonymousMetricsProfiling = const bool.fromEnvironment('WEBF_DEBUG_FLEX_ANON_METRICS', defaultValue: false); static int flexAnonymousMetricsProfilingSummaryEvery = diff --git a/webf/lib/src/rendering/flex.dart b/webf/lib/src/rendering/flex.dart index 2f1ce0c006..5ccb5f91f1 100644 --- a/webf/lib/src/rendering/flex.dart +++ b/webf/lib/src/rendering/flex.dart @@ -129,6 +129,187 @@ typedef _FlexFastPathRejectCallback = void Function( Map? details, }); +enum _FlexAdjustFastPathRejectReason { + wouldGrow, + wouldShrink, +} + +String _flexAdjustFastPathRejectReasonLabel( + _FlexAdjustFastPathRejectReason reason) { + switch (reason) { + case _FlexAdjustFastPathRejectReason.wouldGrow: + return 'wouldGrow'; + case _FlexAdjustFastPathRejectReason.wouldShrink: + return 'wouldShrink'; + } +} + +enum _FlexAdjustFastPathRelayoutReason { + effectiveChildNeedsRelayout, + postMeasureLayout, + pendingIntrinsicInvalidation, + preservedMainMismatch, + autoMainWithNonTightConstraint, + columnAutoCrossOverflow, +} + +String _flexAdjustFastPathRelayoutReasonLabel( + _FlexAdjustFastPathRelayoutReason reason) { + switch (reason) { + case _FlexAdjustFastPathRelayoutReason.effectiveChildNeedsRelayout: + return 'effectiveChildNeedsRelayout'; + case _FlexAdjustFastPathRelayoutReason.postMeasureLayout: + return 'postMeasureLayout'; + case _FlexAdjustFastPathRelayoutReason.pendingIntrinsicInvalidation: + return 'pendingIntrinsicInvalidation'; + case _FlexAdjustFastPathRelayoutReason.preservedMainMismatch: + return 'preservedMainMismatch'; + case _FlexAdjustFastPathRelayoutReason.autoMainWithNonTightConstraint: + return 'autoMainWithNonTightConstraint'; + case _FlexAdjustFastPathRelayoutReason.columnAutoCrossOverflow: + return 'columnAutoCrossOverflow'; + } +} + +class _FlexAdjustFastPathProfiler { + static int _attempts = 0; + static int _hits = 0; + static int _detailLogs = 0; + static int _relayoutRows = 0; + static int _relayoutChildren = 0; + static final Map<_FlexAdjustFastPathRejectReason, int> _rejectCounts = + <_FlexAdjustFastPathRejectReason, int>{}; + static final Map<_FlexAdjustFastPathRelayoutReason, int> _relayoutCounts = + <_FlexAdjustFastPathRelayoutReason, int>{}; + + static bool get enabled => DebugFlags.enableFlexAdjustFastPathProfiling; + + static int get _summaryEvery { + final int configured = DebugFlags.flexAdjustFastPathProfilingSummaryEvery; + return configured > 0 ? configured : 50; + } + + static int get _maxDetailLogs { + final int configured = DebugFlags.flexAdjustFastPathProfilingMaxDetailLogs; + return configured >= 0 ? configured : 0; + } + + static void recordReject( + String path, + _FlexAdjustFastPathRejectReason reason, { + Map? details, + }) { + if (!enabled) return; + _attempts++; + _rejectCounts.update(reason, (int value) => value + 1, ifAbsent: () => 1); + if (_detailLogs < _maxDetailLogs) { + final StringBuffer message = StringBuffer() + ..write('[FlexAdjustFastPath][reject] path=') + ..write(path) + ..write(' reason=') + ..write(_flexAdjustFastPathRejectReasonLabel(reason)); + if (details != null && details.isNotEmpty) { + message + ..write(' details=') + ..write(_formatDetails(details)); + } + renderingLogger.info(message.toString()); + _detailLogs++; + } + _maybeLogSummary(); + } + + static void recordRelayout( + String path, + String childLabel, + _FlexAdjustFastPathRelayoutReason reason, { + BoxConstraints? childConstraints, + Map? details, + }) { + if (!enabled) return; + _relayoutChildren++; + _relayoutCounts.update(reason, (int value) => value + 1, ifAbsent: () => 1); + if (_detailLogs < _maxDetailLogs) { + final StringBuffer message = StringBuffer() + ..write('[FlexAdjustFastPath][relayout] path=') + ..write(path) + ..write(' child=') + ..write(childLabel) + ..write(' reason=') + ..write(_flexAdjustFastPathRelayoutReasonLabel(reason)); + if (childConstraints != null) { + message + ..write(' constraints=') + ..write(childConstraints); + } + if (details != null && details.isNotEmpty) { + message + ..write(' details=') + ..write(_formatDetails(details)); + } + renderingLogger.info(message.toString()); + _detailLogs++; + } + } + + static void recordHit( + String path, { + required int relayoutRowCount, + required int relayoutChildCount, + }) { + if (!enabled) return; + _attempts++; + _hits++; + _relayoutRows += relayoutRowCount; + _relayoutChildren += relayoutChildCount; + _maybeLogSummary(); + } + + static void _maybeLogSummary() { + if (!enabled) return; + if (_attempts == 0 || _attempts % _summaryEvery != 0) return; + + final int rejects = _attempts - _hits; + final double hitRate = _attempts == 0 ? 0.0 : (_hits / _attempts) * 100.0; + final String rejectSummary = _formatCounts( + _rejectCounts, + _flexAdjustFastPathRejectReasonLabel, + ); + final String relayoutSummary = _formatCounts( + _relayoutCounts, + _flexAdjustFastPathRelayoutReasonLabel, + ); + + renderingLogger.info( + '[FlexAdjustFastPath][summary] attempts=$_attempts hits=$_hits ' + 'hitRate=${hitRate.toStringAsFixed(1)}% rejects=$rejects ' + 'relayoutRows=$_relayoutRows relayoutChildren=$_relayoutChildren ' + 'rejectReasons=$rejectSummary relayoutReasons=$relayoutSummary', + ); + } + + static String _formatCounts( + Map counts, + String Function(T value) labelFor, + ) { + if (counts.isEmpty) { + return 'none'; + } + final List> entries = counts.entries.toList() + ..sort((MapEntry a, MapEntry b) => + b.value.compareTo(a.value)); + return entries + .map((MapEntry entry) => '${labelFor(entry.key)}=${entry.value}') + .join(', '); + } + + static String _formatDetails(Map details) { + return details.entries + .map((MapEntry entry) => '${entry.key}=${entry.value}') + .join(', '); + } +} + class _FlexFastPathProfiler { static int _attempts = 0; static int _hits = 0; @@ -1503,6 +1684,25 @@ class RenderFlexLayout extends RenderLayoutBox { } } + bool _hasOnlyTextFlexRelayoutSubtree(RenderObject? node) { + if (node == null) { + return false; + } + if (node is RenderTextBox) { + return true; + } + if (node is RenderEventListener) { + return _hasOnlyTextFlexRelayoutSubtree(node.child); + } + if (node is RenderLayoutBox) { + if (node.childCount != 1) { + return false; + } + return _hasOnlyTextFlexRelayoutSubtree(node.firstChild); + } + return false; + } + void _setFlexRelayoutForTextParent(RenderBoxModel textParentBoxModel) { if (textParentBoxModel.renderStyle.display == CSSDisplay.flex && textParentBoxModel.renderStyle.width.isAuto && @@ -4244,6 +4444,12 @@ class RenderFlexLayout extends RenderLayoutBox { final double containerStyleMin = isHorizontal ? (renderStyle.minWidth.isNotAuto ? renderStyle.minWidth.computedValue : 0.0) : (renderStyle.minHeight.isNotAuto ? renderStyle.minHeight.computedValue : 0.0); + final bool adjustProfilerEnabled = + _FlexAdjustFastPathProfiler.enabled; + final String? adjustProfilerPath = + adjustProfilerEnabled ? _describeFastPathContainer() : null; + int relayoutRowCount = 0; + int relayoutChildCount = 0; // First, verify no run will actually enter flexible length resolution. for (final _RunMetrics metrics in runMetrics) { @@ -4285,6 +4491,18 @@ class RenderFlexLayout extends RenderLayoutBox { } if (willShrink) { + if (adjustProfilerEnabled && adjustProfilerPath != null) { + _FlexAdjustFastPathProfiler.recordReject( + adjustProfilerPath, + _FlexAdjustFastPathRejectReason.wouldShrink, + details: { + 'freeSpace': freeSpace.toStringAsFixed(2), + 'totalSpace': totalSpace.toStringAsFixed(2), + 'maxMainSize': maxMainSize?.toStringAsFixed(2), + 'totalFlexShrink': metrics.totalFlexShrink.toStringAsFixed(2), + }, + ); + } onReject?.call( _FlexFastPathRejectReason.wouldShrink, details: { @@ -4297,6 +4515,21 @@ class RenderFlexLayout extends RenderLayoutBox { return false; } if (willGrow) { + if (adjustProfilerEnabled && adjustProfilerPath != null) { + _FlexAdjustFastPathProfiler.recordReject( + adjustProfilerPath, + _FlexAdjustFastPathRejectReason.wouldGrow, + details: { + 'freeSpace': freeSpace.toStringAsFixed(2), + 'totalSpace': totalSpace.toStringAsFixed(2), + 'maxMainSize': maxMainSize?.toStringAsFixed(2), + 'totalFlexGrow': metrics.totalFlexGrow.toStringAsFixed(2), + 'boundedOnly': boundedOnly, + 'isMainSizeDefinite': isMainSizeDefinite, + 'containerStyleMin': containerStyleMin.toStringAsFixed(2), + }, + ); + } onReject?.call( _FlexFastPathRejectReason.wouldGrow, details: { @@ -4327,11 +4560,29 @@ class RenderFlexLayout extends RenderLayoutBox { final double childOldMainSize = _getMainSize(child); final double? desiredPreservedMain = _childrenIntrinsicMainSizes[child]; + _FlexAdjustFastPathRelayoutReason? relayoutReason; + BoxConstraints? relayoutConstraints; + Map? relayoutDetails; - bool needsLayout = effectiveChild.needsRelayout || - (_childrenRequirePostMeasureLayout[child] == true); - if (!needsLayout && desiredPreservedMain != null && desiredPreservedMain != childOldMainSize) { + bool needsLayout = false; + if (effectiveChild.needsRelayout) { + needsLayout = true; + relayoutReason = + _FlexAdjustFastPathRelayoutReason.effectiveChildNeedsRelayout; + } else if (_childrenRequirePostMeasureLayout[child] == true) { + needsLayout = true; + relayoutReason = _FlexAdjustFastPathRelayoutReason.postMeasureLayout; + } else if (_subtreeHasPendingIntrinsicMeasureInvalidation(child)) { needsLayout = true; + relayoutReason = + _FlexAdjustFastPathRelayoutReason.pendingIntrinsicInvalidation; + relayoutDetails = + _describeFirstPendingIntrinsicMeasureInvalidation(child); + } else if (desiredPreservedMain != null && + desiredPreservedMain != childOldMainSize) { + needsLayout = true; + relayoutReason = + _FlexAdjustFastPathRelayoutReason.preservedMainMismatch; } if (!needsLayout && desiredPreservedMain != null) { final BoxConstraints applied = child.constraints; @@ -4340,7 +4591,34 @@ class RenderFlexLayout extends RenderLayoutBox { : effectiveChild.renderStyle.height.isAuto; final bool wasNonTightMain = isHorizontal ? !applied.hasTightWidth : !applied.hasTightHeight; if (autoMain && wasNonTightMain) { - needsLayout = true; + final bool preservedMainMatches = + (desiredPreservedMain - childOldMainSize).abs() < 0.5; + final bool textOnlySubtree = _hasOnlyTextFlexRelayoutSubtree(child); + final BoxConstraints candidateConstraints = _getChildAdjustedConstraints( + effectiveChild, + null, + null, + runChildrenCount, + preserveMainAxisSize: desiredPreservedMain, + ); + final double candidateMainMax = isHorizontal + ? candidateConstraints.maxWidth + : candidateConstraints.maxHeight; + final bool fitsCandidateMain = + !candidateMainMax.isFinite || + candidateMainMax + 0.5 >= childOldMainSize; + final bool reusesAppliedConstraints = + candidateConstraints == applied; + final bool canSkipRelayout = + preservedMainMatches && + (reusesAppliedConstraints || + (textOnlySubtree && fitsCandidateMain)); + if (!canSkipRelayout) { + needsLayout = true; + relayoutReason = + _FlexAdjustFastPathRelayoutReason.autoMainWithNonTightConstraint; + relayoutConstraints = candidateConstraints; + } } } @@ -4351,15 +4629,52 @@ class RenderFlexLayout extends RenderLayoutBox { final double measuredBorderW = _getChildSize(effectiveChild)!.width; if (measuredBorderW > availCross + 0.5) { needsLayout = true; + relayoutReason = + _FlexAdjustFastPathRelayoutReason.columnAutoCrossOverflow; } } } if (!needsLayout) continue; + relayoutReason ??= + _FlexAdjustFastPathRelayoutReason.effectiveChildNeedsRelayout; + if (adjustProfilerEnabled && adjustProfilerPath != null) { + final Map details = { + 'oldMain': childOldMainSize.toStringAsFixed(2), + 'desiredMain': desiredPreservedMain?.toStringAsFixed(2), + 'autoMain': isHorizontal + ? effectiveChild.renderStyle.width.isAuto + : effectiveChild.renderStyle.height.isAuto, + 'appliedMainTight': isHorizontal + ? child.constraints.hasTightWidth + : child.constraints.hasTightHeight, + }; + if (relayoutConstraints != null) { + details['candidateMainMax'] = (isHorizontal + ? relayoutConstraints.maxWidth + : relayoutConstraints.maxHeight) + .toStringAsFixed(2); + } + if (!isHorizontal && availCross.isFinite) { + details['availCross'] = availCross.toStringAsFixed(2); + } + if (relayoutDetails != null && relayoutDetails.isNotEmpty) { + details.addAll(relayoutDetails); + } + _FlexAdjustFastPathProfiler.recordRelayout( + adjustProfilerPath, + _describeFastPathChild(child), + relayoutReason, + childConstraints: child.constraints, + details: details, + ); + } + relayoutChildCount++; _markFlexRelayoutForTextOnly(effectiveChild); - final BoxConstraints childConstraints = _getChildAdjustedConstraints( + final BoxConstraints childConstraints = relayoutConstraints ?? + _getChildAdjustedConstraints( effectiveChild, null, // no main-axis flexing null, // no cross-axis stretching @@ -4367,10 +4682,12 @@ class RenderFlexLayout extends RenderLayoutBox { preserveMainAxisSize: desiredPreservedMain, ); _layoutChildForFlex(child, childConstraints); + _clearSubtreeIntrinsicMeasurementInvalidationAfterMeasurement(child); didRelayout = true; } if (!didRelayout) continue; + relayoutRowCount++; // Recompute run extents from the final child sizes. double mainAxisExtent = 0; @@ -4385,6 +4702,13 @@ class RenderFlexLayout extends RenderLayoutBox { metrics.crossAxisExtent = crossAxisExtent; } + if (adjustProfilerEnabled && adjustProfilerPath != null) { + _FlexAdjustFastPathProfiler.recordHit( + adjustProfilerPath, + relayoutRowCount: relayoutRowCount, + relayoutChildCount: relayoutChildCount, + ); + } return true; } From 08af71710886689f2d86dde3e134ebe96228991c Mon Sep 17 00:00:00 2001 From: andycall Date: Fri, 27 Mar 2026 04:24:32 -0700 Subject: [PATCH 06/13] perf(webf): narrow mixed flex measurement reuse --- webf/lib/src/rendering/flex.dart | 122 ++++++++++++++++++++++++++----- 1 file changed, 103 insertions(+), 19 deletions(-) diff --git a/webf/lib/src/rendering/flex.dart b/webf/lib/src/rendering/flex.dart index 5ccb5f91f1..73a892e71d 100644 --- a/webf/lib/src/rendering/flex.dart +++ b/webf/lib/src/rendering/flex.dart @@ -1703,6 +1703,54 @@ class RenderFlexLayout extends RenderLayoutBox { return false; } + bool _constraintValueClose(double a, double b) { + if (a.isInfinite || b.isInfinite) return a == b; + return (a - b).abs() < 0.5; + } + + bool _tightensMainAxisToCurrentSizeWithoutCrossChange( + BoxConstraints applied, + BoxConstraints candidate, + double currentMainSize, + ) { + final double candidateMainMin = + _isHorizontalFlexDirection ? candidate.minWidth : candidate.minHeight; + final double candidateMainMax = + _isHorizontalFlexDirection ? candidate.maxWidth : candidate.maxHeight; + final double appliedMainMin = + _isHorizontalFlexDirection ? applied.minWidth : applied.minHeight; + final double appliedMainMax = + _isHorizontalFlexDirection ? applied.maxWidth : applied.maxHeight; + final double candidateCrossMin = + _isHorizontalFlexDirection ? candidate.minHeight : candidate.minWidth; + final double candidateCrossMax = + _isHorizontalFlexDirection ? candidate.maxHeight : candidate.maxWidth; + final double appliedCrossMin = + _isHorizontalFlexDirection ? applied.minHeight : applied.minWidth; + final double appliedCrossMax = + _isHorizontalFlexDirection ? applied.maxHeight : applied.maxWidth; + + final bool crossUnchanged = + _constraintValueClose(candidateCrossMin, appliedCrossMin) && + _constraintValueClose(candidateCrossMax, appliedCrossMax); + if (!crossUnchanged) { + return false; + } + + final bool candidatePinsCurrentSize = candidateMainMax.isFinite && + (candidateMainMax - currentMainSize).abs() < 0.5 && + candidateMainMin <= currentMainSize + 0.5; + if (!candidatePinsCurrentSize) { + return false; + } + + final bool currentSatisfiesApplied = + appliedMainMin <= currentMainSize + 0.5 && + (!appliedMainMax.isFinite || + currentMainSize <= appliedMainMax + 0.5); + return currentSatisfiesApplied; + } + void _setFlexRelayoutForTextParent(RenderBoxModel textParentBoxModel) { if (textParentBoxModel.renderStyle.display == CSSDisplay.flex && textParentBoxModel.renderStyle.width.isAuto && @@ -2447,10 +2495,14 @@ class RenderFlexLayout extends RenderLayoutBox { bool _canUseAnonymousMetricsOnlyCache(List children) { int metricsOnlyChildCount = 0; + int flexingMetricsOnlyChildCount = 0; for (int childIndex = 0; childIndex < children.length; childIndex++) { final RenderBox child = children[childIndex]; if (_isMetricsOnlyIntrinsicMeasureChild(child)) { metricsOnlyChildCount++; + if (_getFlexGrow(child) > 0 || _getFlexShrink(child) > 0) { + flexingMetricsOnlyChildCount++; + } } final _FlexAnonymousMetricsRejectReason? rejectReason = @@ -2471,12 +2523,14 @@ class RenderFlexLayout extends RenderLayoutBox { ); return false; } - if (metricsOnlyChildCount != children.length) { + if (metricsOnlyChildCount != children.length && + flexingMetricsOnlyChildCount > 0) { _recordAnonymousMetricsReject( _FlexAnonymousMetricsRejectReason.mixedMetricsOnlyChildren, details: { 'childCount': children.length, 'candidateChildCount': metricsOnlyChildCount, + 'flexingCandidateChildCount': flexingMetricsOnlyChildCount, }, ); return false; @@ -3125,8 +3179,11 @@ class RenderFlexLayout extends RenderLayoutBox { bool _canAttemptFullEarlyFastPath(List<_RunMetrics> runMetrics) { for (final _RunMetrics metrics in runMetrics) { + final bool hasFlexibleLengths = + metrics.totalFlexGrow > 0 || metrics.totalFlexShrink > 0; for (final _RunChild runChild in metrics.runChildren) { - if (!_hasEffectivelyTightMainAxisSize(runChild)) { + if (hasFlexibleLengths && + !_hasEffectivelyTightMainAxisSize(runChild)) { _recordEarlyFastPathReject( _FlexFastPathRejectReason.childNonTightWidth, child: runChild.child, @@ -3174,6 +3231,15 @@ class RenderFlexLayout extends RenderLayoutBox { return false; } + final double constraintMaxMainAxisSize = _isHorizontalFlexDirection + ? childConstraints.maxWidth + : childConstraints.maxHeight; + if (constraintMaxMainAxisSize.isFinite && + constraintMaxMainAxisSize > 0 && + (usedMainAxisSize - constraintMaxMainAxisSize).abs() < 0.5) { + return true; + } + final double resolvedMainAxisSize = explicitMainSize.computedValue; if (!resolvedMainAxisSize.isFinite || resolvedMainAxisSize <= 0) { return false; @@ -3193,13 +3259,16 @@ class RenderFlexLayout extends RenderLayoutBox { } for (final _RunMetrics metrics in runMetrics) { + final bool hasFlexibleLengths = + metrics.totalFlexGrow > 0 || metrics.totalFlexShrink > 0; for (final _RunChild runChild in metrics.runChildren) { final RenderBoxModel? effectiveChild = runChild.effectiveChild; if (effectiveChild == null) { return false; } - if (!_hasEffectivelyTightMainAxisSize(runChild)) { + if (hasFlexibleLengths && + !_hasEffectivelyTightMainAxisSize(runChild)) { return false; } } @@ -3208,6 +3277,27 @@ class RenderFlexLayout extends RenderLayoutBox { return true; } + void _storePercentageConstraintChildrenOldConstraints(List children) { + for (final RenderBox child in children) { + final RenderBoxModel? box = child is RenderBoxModel + ? child + : (child is RenderEventListener ? child.child as RenderBoxModel? : null); + if (box == null) { + continue; + } + + final bool hasPercentageMaxWidth = + box.renderStyle.maxWidth.type == CSSLengthType.PERCENTAGE; + final bool hasPercentageMaxHeight = + box.renderStyle.maxHeight.type == CSSLengthType.PERCENTAGE; + if (!hasPercentageMaxWidth && !hasPercentageMaxHeight) { + continue; + } + + _childrenOldConstraints[box] = box.getConstraints(); + } + } + List<_RunMetrics>? _tryBuildEarlyNoFlexNoStretchNoBaselineRunMetrics(List children) { if (!_isHorizontalFlexDirection) { _recordEarlyFastPathReject( @@ -3292,6 +3382,7 @@ class RenderFlexLayout extends RenderLayoutBox { ]; _flexLineBoxMetrics = runMetrics; + _storePercentageConstraintChildrenOldConstraints(children); return runMetrics; } @@ -4133,22 +4224,8 @@ class RenderFlexLayout extends RenderLayoutBox { _flexLineBoxMetrics = runMetrics; // PASS 3: Store percentage constraints for later use in _adjustChildrenSize - // This ensures they are calculated with the final parent dimensions - for (RenderBox child in children) { - RenderBoxModel? box = child is RenderBoxModel - ? child - : (child is RenderEventListener ? child.child as RenderBoxModel? : null); - if (box != null) { - bool hasPercentageMaxWidth = box.renderStyle.maxWidth.type == CSSLengthType.PERCENTAGE; - bool hasPercentageMaxHeight = box.renderStyle.maxHeight.type == CSSLengthType.PERCENTAGE; - - if (hasPercentageMaxWidth || hasPercentageMaxHeight) { - // Store the final constraints for use in _adjustChildrenSize - BoxConstraints finalConstraints = box.getConstraints(); - _childrenOldConstraints[box] = finalConstraints; - } - } - } + // This ensures they are calculated with the final parent dimensions. + _storePercentageConstraintChildrenOldConstraints(children); return runMetrics; } @@ -4609,9 +4686,16 @@ class RenderFlexLayout extends RenderLayoutBox { candidateMainMax + 0.5 >= childOldMainSize; final bool reusesAppliedConstraints = candidateConstraints == applied; + final bool tightensToCurrentSize = + _tightensMainAxisToCurrentSizeWithoutCrossChange( + applied, + candidateConstraints, + childOldMainSize, + ); final bool canSkipRelayout = preservedMainMatches && (reusesAppliedConstraints || + tightensToCurrentSize || (textOnlySubtree && fitsCandidateMain)); if (!canSkipRelayout) { needsLayout = true; From cbcd4013a91fbcb92c8431f6c3c5a11782f30d2c Mon Sep 17 00:00:00 2001 From: andycall Date: Fri, 27 Mar 2026 05:35:25 -0700 Subject: [PATCH 07/13] perf(webf): skip futile early flex run-metrics passes --- webf/lib/src/rendering/flex.dart | 41 +++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/webf/lib/src/rendering/flex.dart b/webf/lib/src/rendering/flex.dart index 73a892e71d..c969805ead 100644 --- a/webf/lib/src/rendering/flex.dart +++ b/webf/lib/src/rendering/flex.dart @@ -2493,6 +2493,42 @@ class RenderFlexLayout extends RenderLayoutBox { return true; } + bool _shouldSkipEarlyNoFlexNoStretchNoBaselineRunMetrics( + List children, + ) { + if (!_isHorizontalFlexDirection || renderStyle.flexWrap != FlexWrap.nowrap) { + return true; + } + + for (final RenderBox child in children) { + if (child is RenderPositionPlaceholder) { + continue; + } + + final double flexGrow = _getFlexGrow(child); + final double flexShrink = _getFlexShrink(child); + if (flexGrow <= 0 && flexShrink <= 0) { + continue; + } + + final RenderBoxModel? box = child is RenderBoxModel + ? child + : (child is RenderEventListener ? child.child as RenderBoxModel? : null); + if (box == null) { + return true; + } + + final double? flexBasis = _getFlexBasis(child); + final CSSLengthValue explicitMainSize = + _isHorizontalFlexDirection ? box.renderStyle.width : box.renderStyle.height; + if (flexBasis == null && explicitMainSize.isAuto) { + return true; + } + } + + return false; + } + bool _canUseAnonymousMetricsOnlyCache(List children) { int metricsOnlyChildCount = 0; int flexingMetricsOnlyChildCount = 0; @@ -3544,7 +3580,10 @@ class RenderFlexLayout extends RenderLayoutBox { return; } - List<_RunMetrics>? runMetrics = _tryBuildEarlyNoFlexNoStretchNoBaselineRunMetrics(children); + List<_RunMetrics>? runMetrics; + if (!_shouldSkipEarlyNoFlexNoStretchNoBaselineRunMetrics(children)) { + runMetrics = _tryBuildEarlyNoFlexNoStretchNoBaselineRunMetrics(children); + } if (runMetrics != null) { final bool hasStretchedChildren = _hasStretchedChildrenInCrossAxis(runMetrics); if (!hasStretchedChildren && _canAttemptFullEarlyFastPath(runMetrics)) { From 951e6734476ab5051196519b1ba3791a732dc329 Mon Sep 17 00:00:00 2001 From: andycall Date: Fri, 27 Mar 2026 10:33:16 -0700 Subject: [PATCH 08/13] perf(webf): reduce layout constraint overhead --- .../css-overflow/listview.ts.7cf730ed3.png | Bin 3093 -> 3092 bytes webf/lib/src/css/values/length.dart | 5 + webf/lib/src/rendering/box_model.dart | 135 +++++++++--------- 3 files changed, 72 insertions(+), 68 deletions(-) diff --git a/integration_tests/snapshots/css/css-overflow/listview.ts.7cf730ed3.png b/integration_tests/snapshots/css/css-overflow/listview.ts.7cf730ed3.png index 28d64b3af21ee30f1e36c475fa25c1db6d638746..72d752edbf441c006a467b8d97ca52e1c09ddcc9 100644 GIT binary patch literal 3092 zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX< z4B-HR8jh3>1_thZo-U3d6?5L+J=;AuobkZJVs+!rlQSYjeWJ9F_KO_#ed#2!utPy< z*9kX)N2|L3FuuF<%87GFLF%N|4iE8NMvsndozO9ZD=MI=qkBe|u4##pqVdd~dU>;~ z&iL5znEzC*oOiXr@_X(1oVsF*?|aJCmiy12$M;}j$)h8k?Y6AXG9D)SGc?TA-S@e9 z|B(+bEQ+6idT*3O?Vp|9Sb>k8}C!^QHG2 zNIq_luR8zZ)$#53(n@Q4^8dZM_2cn=vvq~pJn^L zx7Kcd^yj*{`SG3J(#cx=aDWsk3{&VKykX7=M3 zE2n^t^`%8+UGALSTMW(KZD{|q)nBJ?X{({x^5j1c%Cn=_eSE!tZNK3$i*J8wa~qj$ zu01R3dc4^Gtlr;WS6^v`W;Dp2)x9wN-n_Lv<-Zg@9BxnBE}GtO7bsR=S-h>u zZja5)Z;1~!F5djObbGap%mYuLJN`d=Z+`4m@6Ec;c9rp8WA?|5>KLr?Ovv z-er40Ie(ph*Pl1%Uq3#=?fv-m{1_?y+LXdIYy6KFw~HNnbVwpwI{D9wL$~JYGks{; zwccU>X2*^DPRD)vem+)y_T`rr9}nFttf(~NyKl1}ls@)-Je^%$BPIC2vn;;x-y`kS zK|*zxTYoro`t;|yoD9)-N*Bz(Wj^!055tW^V(e(y8YyGmtqOR0W~MQJxf(-G zA}g8IR0EhAp> SQvleqXYh3Ob6Mw<&;$SjvUXGe literal 3093 zcmeAS@N?(olHy`uVBq!ia0y~yV9a1(U~1rC1Bz^vdbAu!F%}28J29*~C-V}>VJUX< z4B-HR8jh3>1_ti^o-U3d6?5L+_3fA&&e-s{U4OwIRAZuXG_x%nqZOQ#(o)YAff(aCdfuU2sxev7=+`&}a%O63>iXpeWOUzrWns8+^O3dv4{x6S zzN=TS?w!ZvF2%O0QF`f}Z4yMhEGyWd~- zeO%Z)`D6R_ZO63N=l8$8+s^!9L8ILc6UqBO?|l04*`C3&!3K2 zl)igqQJ9wd=kuv=@5JZz6n@&d)bN-@dfYvU_@6(2fuw)@@xL$kdUo=gXo<(S=imJQ zVL#)6Ysp#)Fod4ZY7dw9Gki_lFhIK!7_ugLj@%8@PKEq=c zKkqo_HZt2>dsf!(j{q<*sYEyom6aDm2diCqKFD0(aw6fi}c!#O><=UI4Dwgvz+1HemMHjN& zxO<1`?v8hZd+w(53zn$6UCD7dae;)q=2HWlYfA_k#%XwJVWf<-I zvoUf(d{t_3+;;YgCuhHUPMrTYwRp{*4JP~k{kc@}QMo%gx7cc)?Jff`@nbh`NIgDY ze`m*cSB7~3{Gu<)%{0u)Z+ibb_%l1X+DcYiU#CC0UG4FW-p|sY1R}kEPu2P5f{gQ0 zfNJ&kZK*qNxLiM{=d!rjvCi-MJXkP(Ex#0oui3iG%<`O21HYG enB|9G`Yq+BY}+Su-3K=989ZJ6T-G@yGywomj)C0( diff --git a/webf/lib/src/css/values/length.dart b/webf/lib/src/css/values/length.dart index bf0ba3f867..b4b97a10de 100644 --- a/webf/lib/src/css/values/length.dart +++ b/webf/lib/src/css/values/length.dart @@ -329,6 +329,11 @@ class CSSLengthValue { // which can not be computed to a specific value, eg. percentage height is sometimes parsed // to be auto due to parent height not defined. double get computedValue { + if (calcValue == null && type == CSSLengthType.PX) { + _computedValue = value ?? 0; + return _computedValue!; + } + if (calcValue != null) { _computedValue = calcValue!.computedValue(propertyName ?? '') ?? 0; return _computedValue!; diff --git a/webf/lib/src/rendering/box_model.dart b/webf/lib/src/rendering/box_model.dart index e2c483ecf1..0734b69483 100644 --- a/webf/lib/src/rendering/box_model.dart +++ b/webf/lib/src/rendering/box_model.dart @@ -507,43 +507,43 @@ abstract class RenderBoxModel extends RenderBox // Calculate constraints of renderBoxModel on layout stage and // only needed to be executed once on every layout. BoxConstraints getConstraints() { - CSSDisplay? effectiveDisplay = renderStyle.effectiveDisplay; + final CSSRenderStyle style = renderStyle; + final CSSRenderStyle? attachedParentStyle = + style.getAttachedRenderParentRenderStyle(); + final RenderBoxModel? attachedParentRenderBoxModel = + attachedParentStyle?.attachedRenderBoxModel; + + CSSDisplay? effectiveDisplay = style.effectiveDisplay; bool isDisplayInline = effectiveDisplay == CSSDisplay.inline; double? minWidth = - renderStyle.minWidth.isAuto ? null : renderStyle.minWidth.computedValue; + style.minWidth.isAuto ? null : style.minWidth.computedValue; double? maxWidth = - renderStyle.maxWidth.isNone ? null : renderStyle.maxWidth.computedValue; - double? minHeight = renderStyle.minHeight.isAuto + style.maxWidth.isNone ? null : style.maxWidth.computedValue; + double? minHeight = style.minHeight.isAuto ? null - : renderStyle.minHeight.computedValue; - double? maxHeight = renderStyle.maxHeight.isNone + : style.minHeight.computedValue; + double? maxHeight = style.maxHeight.isNone ? null - : renderStyle.maxHeight.computedValue; + : style.maxHeight.computedValue; // Need to calculated logic content size on every layout. - renderStyle.computeContentBoxLogicalWidth(); - renderStyle.computeContentBoxLogicalHeight(); + style.computeContentBoxLogicalWidth(); + style.computeContentBoxLogicalHeight(); // Width should be not smaller than border and padding in horizontal direction // when box-sizing is border-box which is only supported. double minConstraintWidth = - renderStyle.effectiveBorderLeftWidth.computedValue + - renderStyle.effectiveBorderRightWidth.computedValue + - renderStyle.paddingLeft.computedValue + - renderStyle.paddingRight.computedValue; + style.effectiveBorderLeftWidth.computedValue + + style.effectiveBorderRightWidth.computedValue + + style.paddingLeft.computedValue + + style.paddingRight.computedValue; double? parentBoxContentConstraintsWidth; - if (renderStyle.isParentRenderBoxModel() && - renderStyle - .getAttachedRenderParentRenderStyle() - ?.attachedRenderBoxModel != - null && - (renderStyle.isSelfRenderLayoutBox() || - renderStyle.isSelfRenderWidget())) { - RenderBoxModel parentRenderBoxModel = (renderStyle - .getAttachedRenderParentRenderStyle()! - .attachedRenderBoxModel!); + if (style.isParentRenderBoxModel() && + attachedParentRenderBoxModel != null && + (style.isSelfRenderLayoutBox() || style.isSelfRenderWidget())) { + RenderBoxModel parentRenderBoxModel = attachedParentRenderBoxModel; // Inline-block shrink-to-fit: when the parent is inline-block with auto width, // do not bound block children by the parent's finite content width. This allows @@ -584,10 +584,9 @@ abstract class RenderBoxModel extends RenderBox } // Flex context adjustments - if (renderStyle.isParentRenderFlexLayout()) { - final RenderFlexLayout flexParent = renderStyle - .getAttachedRenderParentRenderStyle()! - .attachedRenderBoxModel! as RenderFlexLayout; + if (style.isParentRenderFlexLayout()) { + final RenderFlexLayout flexParent = + attachedParentStyle!.attachedRenderBoxModel! as RenderFlexLayout; // FlexItems with flex:none won't inherit parent box's constraints if (flexParent.isFlexNone(this)) { parentBoxContentConstraintsWidth = null; @@ -622,11 +621,11 @@ abstract class RenderBoxModel extends RenderBox // In such cases, the containing block still exists, but the abspos box should size // from its own content rather than the flex item's zero content width. final bool isAbsOrFixed = - renderStyle.position == CSSPositionType.absolute || - renderStyle.position == CSSPositionType.fixed; + style.position == CSSPositionType.absolute || + style.position == CSSPositionType.fixed; if (isAbsOrFixed && - renderStyle.width.isAuto && - renderStyle.isParentRenderFlexLayout() && + style.width.isAuto && + style.isParentRenderFlexLayout() && parentBoxContentConstraintsWidth != null && parentBoxContentConstraintsWidth == 0) { parentBoxContentConstraintsWidth = null; @@ -634,10 +633,11 @@ abstract class RenderBoxModel extends RenderBox } else if (isDisplayInline && parent is RenderFlowLayout) { // For inline elements inside a flow layout, check if we should inherit parent's constraints RenderFlowLayout parentFlow = parent as RenderFlowLayout; + final CSSRenderStyle parentFlowStyle = parentFlow.renderStyle; // Skip constraint inheritance if parent is a flex item with flex: none (flex-grow: 0, flex-shrink: 0) - if (parentFlow.renderStyle.isParentRenderFlexLayout()) { - RenderFlexLayout flexParent = parentFlow.renderStyle + if (parentFlowStyle.isParentRenderFlexLayout()) { + RenderFlexLayout flexParent = parentFlowStyle .getAttachedRenderParentRenderStyle()! .attachedRenderBoxModel as RenderFlexLayout; if (flexParent.isFlexNone(parentFlow)) { @@ -645,7 +645,7 @@ abstract class RenderBoxModel extends RenderBox parentBoxContentConstraintsWidth = null; } else { double parentContentWidth = - parentFlow.renderStyle.contentMaxConstraintsWidth; + parentFlowStyle.contentMaxConstraintsWidth; if (parentContentWidth != double.infinity) { parentBoxContentConstraintsWidth = parentContentWidth; } @@ -653,7 +653,7 @@ abstract class RenderBoxModel extends RenderBox } else { // Not in a flex context, inherit parent's content width constraint normally double parentContentWidth = - parentFlow.renderStyle.contentMaxConstraintsWidth; + parentFlowStyle.contentMaxConstraintsWidth; if (parentContentWidth != double.infinity) { parentBoxContentConstraintsWidth = parentContentWidth; } @@ -671,21 +671,20 @@ abstract class RenderBoxModel extends RenderBox // explicit min/max-width), which matches shrink-to-fit in the common // case where content width <= available width. final bool absOrFixedForWidth = - renderStyle.position == CSSPositionType.absolute || - renderStyle.position == CSSPositionType.fixed; - final bool widthAutoForAbs = renderStyle.width.isAuto; - final bool bothLRNonAuto = - renderStyle.left.isNotAuto && renderStyle.right.isNotAuto; + style.position == CSSPositionType.absolute || + style.position == CSSPositionType.fixed; + final bool widthAutoForAbs = style.width.isAuto; + final bool bothLRNonAuto = style.left.isNotAuto && style.right.isNotAuto; if (absOrFixedForWidth && !bothLRNonAuto && widthAutoForAbs && - !renderStyle.isSelfRenderReplaced() && - renderStyle.borderBoxLogicalWidth == null && + !style.isSelfRenderReplaced() && + style.borderBoxLogicalWidth == null && parentBoxContentConstraintsWidth != null) { parentBoxContentConstraintsWidth = null; } - double maxConstraintWidth = renderStyle.borderBoxLogicalWidth ?? + double maxConstraintWidth = style.borderBoxLogicalWidth ?? parentBoxContentConstraintsWidth ?? double.infinity; @@ -695,14 +694,14 @@ abstract class RenderBoxModel extends RenderBox // with auto width) but the containing block has been measured. See: // https://www.w3.org/TR/css-position-3/#abs-non-replaced-width final bool isAbsOrFixed = - renderStyle.position == CSSPositionType.absolute || - renderStyle.position == CSSPositionType.fixed; + style.position == CSSPositionType.absolute || + style.position == CSSPositionType.fixed; if (maxConstraintWidth == double.infinity && isAbsOrFixed && - !renderStyle.isSelfRenderReplaced() && - renderStyle.width.isAuto && - renderStyle.left.isNotAuto && - renderStyle.right.isNotAuto && + !style.isSelfRenderReplaced() && + style.width.isAuto && + style.left.isNotAuto && + style.right.isNotAuto && parent is RenderBoxModel) { final RenderBoxModel cb = parent as RenderBoxModel; double? parentPaddingBoxWidth; @@ -718,10 +717,10 @@ abstract class RenderBoxModel extends RenderBox if (parentPaddingBoxWidth != null && parentPaddingBoxWidth.isFinite) { // Solve the horizontal insets equation for the child border-box width. double solvedBorderBoxWidth = parentPaddingBoxWidth - - renderStyle.left.computedValue - - renderStyle.right.computedValue - - renderStyle.marginLeft.computedValue - - renderStyle.marginRight.computedValue; + style.left.computedValue - + style.right.computedValue - + style.marginLeft.computedValue - + style.marginRight.computedValue; // Guard against negative sizes. solvedBorderBoxWidth = math.max(0, solvedBorderBoxWidth); // Use a tight width so empty positioned boxes still fill the available space. @@ -736,15 +735,15 @@ abstract class RenderBoxModel extends RenderBox // CSSLengthValue.computedValue returns 0 for intrinsic keywords, so treating them as // definite lengths during style computation collapses boxes to padding/border only. // Here we compute the used border-box width and tighten constraints to that value. - if (renderStyle.width.isIntrinsic && + if (style.width.isIntrinsic && !isDisplayInline && // For non-replaced boxes, intrinsic widths come from layout content. - !renderStyle.isSelfRenderReplaced() && + !style.isSelfRenderReplaced() && // Absolutely positioned boxes with both insets specified should be solved by insets. !(isAbsOrFixed && - renderStyle.left.isNotAuto && - renderStyle.right.isNotAuto && - renderStyle.width.isAuto)) { + style.left.isNotAuto && + style.right.isNotAuto && + style.width.isAuto)) { // Use the parent's available inline size if it's definite; otherwise fall back to infinity. final double available = (parentBoxContentConstraintsWidth != null && parentBoxContentConstraintsWidth.isFinite) @@ -757,8 +756,8 @@ abstract class RenderBoxModel extends RenderBox double maxIntrinsic = getMaxIntrinsicWidth(double.infinity); // Respect nowrap/pre: min-content equals max-content for unbreakable inline content. - if (renderStyle.whiteSpace == WhiteSpace.nowrap || - renderStyle.whiteSpace == WhiteSpace.pre) { + if (style.whiteSpace == WhiteSpace.nowrap || + style.whiteSpace == WhiteSpace.pre) { minIntrinsic = maxIntrinsic; } @@ -767,7 +766,7 @@ abstract class RenderBoxModel extends RenderBox maxIntrinsic = minIntrinsic; double used; - switch (renderStyle.width.type) { + switch (style.width.type) { case CSSLengthType.MIN_CONTENT: used = minIntrinsic; break; @@ -790,12 +789,12 @@ abstract class RenderBoxModel extends RenderBox // Height should be not smaller than border and padding in vertical direction // when box-sizing is border-box which is only supported. double minConstraintHeight = - renderStyle.effectiveBorderTopWidth.computedValue + - renderStyle.effectiveBorderBottomWidth.computedValue + - renderStyle.paddingTop.computedValue + - renderStyle.paddingBottom.computedValue; + style.effectiveBorderTopWidth.computedValue + + style.effectiveBorderBottomWidth.computedValue + + style.paddingTop.computedValue + + style.paddingBottom.computedValue; double maxConstraintHeight = - renderStyle.borderBoxLogicalHeight ?? double.infinity; + style.borderBoxLogicalHeight ?? double.infinity; // // Apply maxHeight constraint if specified // if (maxHeight != null && maxHeight < maxConstraintHeight) { @@ -803,9 +802,9 @@ abstract class RenderBoxModel extends RenderBox // } if (parent is RenderFlexLayout) { - double? flexBasis = renderStyle.flexBasis == CSSLengthValue.auto + double? flexBasis = style.flexBasis == CSSLengthValue.auto ? null - : renderStyle.flexBasis?.computedValue; + : style.flexBasis?.computedValue; RenderBoxModel? parentRenderBoxModel = parent as RenderBoxModel?; // In flex layout, flex basis takes priority over width/height if set. // Flex-basis cannot be smaller than its content size which happens can not be known From b5efce08d19f14ab729ea710b3be45684c78f14d Mon Sep 17 00:00:00 2001 From: andycall Date: Fri, 27 Mar 2026 22:15:45 -0700 Subject: [PATCH 09/13] perf(webf): optimize layout hotspots --- webf/lib/src/accessibility/semantics.dart | 55 +- webf/lib/src/css/border.dart | 17 +- webf/lib/src/css/margin.dart | 25 +- webf/lib/src/css/padding.dart | 16 +- webf/lib/src/css/render_style.dart | 186 +++- webf/lib/src/css/text.dart | 122 ++- webf/lib/src/css/values/length.dart | 105 ++- webf/lib/src/rendering/box_model.dart | 88 +- webf/lib/src/rendering/flex.dart | 822 +++++++++++++----- webf/lib/src/rendering/flow.dart | 18 +- .../rendering/inline_formatting_context.dart | 133 ++- webf/lib/src/rendering/layout_box.dart | 33 +- 12 files changed, 1273 insertions(+), 347 deletions(-) diff --git a/webf/lib/src/accessibility/semantics.dart b/webf/lib/src/accessibility/semantics.dart index 66b2aa9270..2f46cc2c8b 100644 --- a/webf/lib/src/accessibility/semantics.dart +++ b/webf/lib/src/accessibility/semantics.dart @@ -67,14 +67,28 @@ class WebFAccessibility { } final bool suppressSelfLabel = _shouldSuppressAutoLabel(element, role); + final String tag = element.tagName.toUpperCase(); + final bool shouldComputeAccessibleName = + !suppressSelfLabel && + _shouldComputeAccessibleName( + element, + tag: tag, + explicitRole: explicitRole, + role: role, + focusable: focusable, + ); + final bool shouldComputeAccessibleDescription = + !suppressSelfLabel && element.hasAttribute('aria-describedby'); // Compute accessible name and description. - if (!suppressSelfLabel) { + if (shouldComputeAccessibleName) { final String? name = computeAccessibleName(element)?.trim(); if (name != null && name.isNotEmpty) { ensureTextDirection(); config.label = name; } + } + if (shouldComputeAccessibleDescription) { final String? hint = computeAccessibleDescription(element)?.trim(); if (hint != null && hint.isNotEmpty) { ensureTextDirection(); @@ -497,6 +511,30 @@ class WebFAccessibility { } } + static bool _shouldComputeAccessibleName( + dom.Element element, { + required String tag, + required String? explicitRole, + required _Role role, + required bool focusable, + }) { + if (element.hasAttribute('aria-label') || + element.hasAttribute('aria-labelledby') || + element.hasAttribute('title')) { + return true; + } + if (role != _Role.none || focusable) { + return true; + } + if (tag == html.IMAGE || + tag == html.ANCHOR || + tag == html.BUTTON || + tag == html.INPUT) { + return true; + } + return _allowsNameFromContent(tag, explicitRole); + } + static bool _hasNonWhitespaceDirectText(dom.Element element) { dom.Node? child = element.firstChild; while (child != null) { @@ -528,6 +566,18 @@ class WebFAccessibility { /// Collect plain text recursively from descendant text nodes. static String _collectText(dom.Node node) { + if (node is dom.TextNode) { + return node.data.trim(); + } + + final dom.Node? first = node.firstChild; + if (first == null) { + return ''; + } + if (first is dom.TextNode && first.nextSibling == null) { + return first.data.trim(); + } + final buffer = StringBuffer(); void walk(dom.Node n) { if (n is dom.TextNode) { @@ -535,8 +585,7 @@ class WebFAccessibility { if (data.isNotEmpty) buffer.write(data); return; } - final dom.Node? first = n.firstChild; - dom.Node? c = first; + dom.Node? c = n.firstChild; while (c != null) { walk(c); c = c.nextSibling; diff --git a/webf/lib/src/css/border.dart b/webf/lib/src/css/border.dart index 451fd12797..960a3cf29b 100644 --- a/webf/lib/src/css/border.dart +++ b/webf/lib/src/css/border.dart @@ -14,6 +14,7 @@ import 'package:webf/css.dart'; import 'package:webf/src/foundation/logger.dart'; import 'package:webf/src/foundation/debug_flags.dart'; import 'package:webf/src/foundation/string_parsers.dart'; +import 'package:webf/rendering.dart'; // Initial border value: medium final CSSLengthValue _mediumWidth = CSSLengthValue(3, CSSLengthType.PX); @@ -93,17 +94,31 @@ extension CSSBorderStyleTypeText on CSSBorderStyleType { } mixin CSSBorderMixin on RenderStyle { + int _layoutPassBorderCachePassId = -1; + EdgeInsets? _layoutPassBorder; + // Effective border widths. These are used to calculate the // dimensions of the border box. @override EdgeInsets get border { + if (renderBoxModelInLayoutStack.isNotEmpty && + _layoutPassBorderCachePassId == renderBoxModelLayoutPassId && + _layoutPassBorder != null) { + return _layoutPassBorder!; + } + // If has border, render padding should subtracting the edge of the border - return EdgeInsets.fromLTRB( + final EdgeInsets edgeInsets = EdgeInsets.fromLTRB( effectiveBorderLeftWidth.computedValue, effectiveBorderTopWidth.computedValue, effectiveBorderRightWidth.computedValue, effectiveBorderBottomWidth.computedValue, ); + if (renderBoxModelInLayoutStack.isNotEmpty) { + _layoutPassBorderCachePassId = renderBoxModelLayoutPassId; + _layoutPassBorder = edgeInsets; + } + return edgeInsets; } Size wrapBorderSize(Size innerSize) { diff --git a/webf/lib/src/css/margin.dart b/webf/lib/src/css/margin.dart index 44761ffcec..9b752b534a 100644 --- a/webf/lib/src/css/margin.dart +++ b/webf/lib/src/css/margin.dart @@ -13,18 +13,31 @@ import 'package:webf/css.dart'; import 'package:webf/rendering.dart'; mixin CSSMarginMixin on RenderStyle { + int _layoutPassMarginCachePassId = -1; + EdgeInsets? _layoutPassMargin; + /// The amount to margin the child in each dimension. /// /// If this is set to an [EdgeInsetsDirectional] object, then [textDirection] /// must not be null. @override EdgeInsets get margin { - EdgeInsets insets = EdgeInsets.only( - left: marginLeft.computedValue, - right: marginRight.computedValue, - bottom: marginBottom.computedValue, - top: marginTop.computedValue) - .resolve(TextDirection.ltr); + if (renderBoxModelInLayoutStack.isNotEmpty && + _layoutPassMarginCachePassId == renderBoxModelLayoutPassId && + _layoutPassMargin != null) { + return _layoutPassMargin!; + } + + final EdgeInsets insets = EdgeInsets.only( + left: marginLeft.computedValue, + right: marginRight.computedValue, + bottom: marginBottom.computedValue, + top: marginTop.computedValue, + ).resolve(TextDirection.ltr); + if (renderBoxModelInLayoutStack.isNotEmpty) { + _layoutPassMarginCachePassId = renderBoxModelLayoutPassId; + _layoutPassMargin = insets; + } return insets; } diff --git a/webf/lib/src/css/padding.dart b/webf/lib/src/css/padding.dart index 1fc57cf4bb..682bcc6d42 100644 --- a/webf/lib/src/css/padding.dart +++ b/webf/lib/src/css/padding.dart @@ -8,8 +8,12 @@ */ import 'package:flutter/rendering.dart'; import 'package:webf/css.dart'; +import 'package:webf/rendering.dart'; mixin CSSPaddingMixin on RenderStyle { + int _layoutPassPaddingCachePassId = -1; + EdgeInsets? _layoutPassPadding; + CSSLengthValue? _normalizePaddingLength(CSSLengthValue? value) { if (value == null) return null; final double? raw = value.value; @@ -29,13 +33,23 @@ mixin CSSPaddingMixin on RenderStyle { /// must not be null. @override EdgeInsets get padding { - EdgeInsets insets = EdgeInsets.only( + if (renderBoxModelInLayoutStack.isNotEmpty && + _layoutPassPaddingCachePassId == renderBoxModelLayoutPassId && + _layoutPassPadding != null) { + return _layoutPassPadding!; + } + + final EdgeInsets insets = EdgeInsets.only( left: _nonNegativePaddingComputedValue(paddingLeft), right: _nonNegativePaddingComputedValue(paddingRight), bottom: _nonNegativePaddingComputedValue(paddingBottom), top: _nonNegativePaddingComputedValue(paddingTop), ); assert(insets.isNonNegative); + if (renderBoxModelInLayoutStack.isNotEmpty) { + _layoutPassPaddingCachePassId = renderBoxModelLayoutPassId; + _layoutPassPadding = insets; + } return insets; } diff --git a/webf/lib/src/css/render_style.dart b/webf/lib/src/css/render_style.dart index 05246ed65f..b3febb84da 100644 --- a/webf/lib/src/css/render_style.dart +++ b/webf/lib/src/css/render_style.dart @@ -742,6 +742,7 @@ abstract class RenderStyle extends DiagnosticableTree with Diagnosticable { BoxFit get objectFit; bool get isHeightStretch; + bool get isWidthStretch; // Transition List get transitionProperty; @@ -804,6 +805,12 @@ abstract class RenderStyle extends DiagnosticableTree with Diagnosticable { RenderObject? _cachedParentLookupDirectParent; RenderObject? _cachedParentLookupRenderObject; RenderStyle? _cachedAttachedParentRenderStyle; + int _cachedViewportBoxLayoutPassId = -1; + RenderBoxModel? _cachedViewportBoxQueryStart; + RenderViewportBox? _cachedViewportBox; + int _cachedAttachedWidgetRenderBoxesLayoutPassId = -1; + List>? + _cachedAttachedWidgetRenderBoxes; Map get widgetRenderObjects => _widgetRenderObjects; @@ -817,6 +824,11 @@ abstract class RenderStyle extends DiagnosticableTree with Diagnosticable { _cachedParentLookupDirectParent = null; _cachedParentLookupRenderObject = null; _cachedAttachedParentRenderStyle = null; + _cachedViewportBoxLayoutPassId = -1; + _cachedViewportBoxQueryStart = null; + _cachedViewportBox = null; + _cachedAttachedWidgetRenderBoxesLayoutPassId = -1; + _cachedAttachedWidgetRenderBoxes = null; } @pragma('vm:prefer-inline') @@ -833,9 +845,21 @@ abstract class RenderStyle extends DiagnosticableTree with Diagnosticable { } } - RenderBoxModel? resolved = - _widgetRenderObjects.values.firstWhereOrNull((renderBox) => renderBox.attached && renderBox.hasSize); - resolved ??= _widgetRenderObjects.values.firstWhereOrNull((renderBox) => renderBox.attached); + RenderBoxModel? resolved; + for (final RenderBoxModel renderBox in _widgetRenderObjects.values) { + if (renderBox.attached && renderBox.hasSize) { + resolved = renderBox; + break; + } + } + if (resolved == null) { + for (final RenderBoxModel renderBox in _widgetRenderObjects.values) { + if (renderBox.attached) { + resolved = renderBox; + break; + } + } + } if (resolved != null) { _cachedAttachedRenderBoxModel = resolved; @@ -1088,38 +1112,34 @@ abstract class RenderStyle extends DiagnosticableTree with Diagnosticable { @pragma('vm:prefer-inline') bool isSelfRenderGridLayout() { - return everyAttachedRenderObjectByTypeAndMatch( - RenderObjectGetType.self, (renderObject, _) => renderObject is RenderGridLayout); + return attachedRenderBoxModel is RenderGridLayout; } @pragma('vm:prefer-inline') bool isSelfRenderFlowLayout() { - return everyAttachedRenderObjectByTypeAndMatch( - RenderObjectGetType.self, (renderObject, _) => renderObject is RenderFlowLayout); + return attachedRenderBoxModel is RenderFlowLayout; } @pragma('vm:prefer-inline') bool isSelfAnonymousFlowLayout() { - return everyAttachedRenderObjectByTypeAndMatch(RenderObjectGetType.self, - (renderObject, _) => renderObject is RenderBoxModel && renderObject.renderStyle.target.tagName == 'Anonymous'); + final RenderBoxModel? renderBoxModel = attachedRenderBoxModel; + return renderBoxModel is RenderFlowLayout && + renderBoxModel.renderStyle.target.tagName == 'Anonymous'; } @pragma('vm:prefer-inline') bool isSelfRenderReplaced() { - return everyAttachedRenderObjectByTypeAndMatch( - RenderObjectGetType.self, (renderObject, _) => renderObject is RenderReplaced); + return attachedRenderBoxModel is RenderReplaced; } @pragma('vm:prefer-inline') bool isSelfRenderWidget() { - return everyAttachedRenderObjectByTypeAndMatch( - RenderObjectGetType.self, (renderObject, _) => renderObject is RenderWidget); + return attachedRenderBoxModel is RenderWidget; } @pragma('vm:prefer-inline') bool isSelfRenderLayoutBox() { - return everyAttachedRenderObjectByTypeAndMatch( - RenderObjectGetType.self, (renderObject, _) => renderObject is RenderLayoutBox); + return attachedRenderBoxModel is RenderLayoutBox; } @pragma('vm:prefer-inline') @@ -1193,14 +1213,34 @@ abstract class RenderStyle extends DiagnosticableTree with Diagnosticable { } RenderViewportBox? getCurrentViewportBox() { - flutter.RenderObject? current = attachedRenderBoxModel; + final RenderBoxModel? currentRenderBoxModel = attachedRenderBoxModel; + if (currentRenderBoxModel == null) return null; + + if (renderBoxModelInLayoutStack.isNotEmpty && + _cachedViewportBoxLayoutPassId == renderBoxModelLayoutPassId && + identical(_cachedViewportBoxQueryStart, currentRenderBoxModel)) { + return _cachedViewportBox; + } + + flutter.RenderObject? current = currentRenderBoxModel; while (current != null) { if (current is RenderViewportBox) { + if (renderBoxModelInLayoutStack.isNotEmpty) { + _cachedViewportBoxLayoutPassId = renderBoxModelLayoutPassId; + _cachedViewportBoxQueryStart = currentRenderBoxModel; + _cachedViewportBox = current; + } return current; } current = current.parent; } + + if (renderBoxModelInLayoutStack.isNotEmpty) { + _cachedViewportBoxLayoutPassId = renderBoxModelLayoutPassId; + _cachedViewportBoxQueryStart = currentRenderBoxModel; + _cachedViewportBox = null; + } return null; } @@ -1682,6 +1722,12 @@ abstract class RenderStyle extends DiagnosticableTree with Diagnosticable { bool everyWidgetRenderBox(EveryRenderBoxModelHandlerCallback callback) { if (_widgetRenderObjects.isEmpty) return false; + if (_widgetRenderObjects.length == 1) { + for (final entry in _widgetRenderObjects.entries) { + return callback(entry.key, entry.value); + } + } + for (var entry in _widgetRenderObjects.entries) { bool result = callback(entry.key, entry.value); if (!result) return false; @@ -1691,6 +1737,45 @@ abstract class RenderStyle extends DiagnosticableTree with Diagnosticable { } bool everyAttachedWidgetRenderBox(EveryRenderBoxModelHandlerCallback callback) { + if (_widgetRenderObjects.isEmpty) return false; + + if (_widgetRenderObjects.length == 1) { + for (final entry in _widgetRenderObjects.entries) { + final RenderBoxModel ro = entry.value; + return ro.attached ? callback(entry.key, ro) : false; + } + } + + final bool canUseLayoutPassCache = renderBoxModelInLayoutStack.isNotEmpty; + if (canUseLayoutPassCache) { + if (_cachedAttachedWidgetRenderBoxesLayoutPassId != + renderBoxModelLayoutPassId) { + _cachedAttachedWidgetRenderBoxesLayoutPassId = + renderBoxModelLayoutPassId; + final List> + attachedEntries = + >[]; + for (final entry in _widgetRenderObjects.entries) { + final RenderBoxModel ro = entry.value; + if (ro.attached) { + attachedEntries.add(MapEntry(entry.key, ro)); + } + } + _cachedAttachedWidgetRenderBoxes = attachedEntries; + } + + final List> + attachedEntries = + _cachedAttachedWidgetRenderBoxes ?? + const >[]; + for (final entry in attachedEntries) { + if (!callback(entry.key, entry.value)) { + return false; + } + } + return true; + } + for (final entry in _widgetRenderObjects.entries) { final ro = entry.value; if (ro.attached && !callback(entry.key, ro)) { @@ -3112,7 +3197,6 @@ class CSSRenderStyle extends RenderStyle // RenderBoxModel current = renderBoxModel!; RenderStyle renderStyle = this; double? logicalWidth; - CSSDisplay? effectiveDisplay = renderStyle.effectiveDisplay; // Special handling for absolutely/fixed positioned non-replaced elements. @@ -3182,6 +3266,16 @@ class CSSRenderStyle extends RenderStyle } else if (logicalWidth == null && (renderStyle.isSelfRouterLinkElement() && root != null)) { logicalWidth = root.boxSize!.width; } else if (logicalWidth == null && parentStyle != null) { + if (renderStyle.isWidthStretch) { + logicalWidth = parentStyle.contentBoxLogicalWidth; + if (logicalWidth != null) { + logicalWidth -= renderStyle.margin.horizontal; + } + } + + if (logicalWidth != null) { + // already resolved by cross-axis stretch + } else { bool isRenderSubtreeAncestor(flutter.RenderObject? ancestor, flutter.RenderObject? node) { if (ancestor == null || node == null) return false; flutter.RenderObject? current = node.parent; @@ -3320,6 +3414,7 @@ class CSSRenderStyle extends RenderStyle } } } + } } } else if (effectiveDisplay == CSSDisplay.inlineBlock || effectiveDisplay == CSSDisplay.inlineFlex || @@ -3566,6 +3661,63 @@ class CSSRenderStyle extends RenderStyle return isStretch; } + @override + bool get isWidthStretch { + RenderStyle renderStyle = this; + CSSRenderStyle? parentRenderStyle = + renderStyle.getAttachedRenderParentRenderStyle(); + if (parentRenderStyle == null) { + return false; + } + + final BoxConstraints? parentContentConstraints = + parentRenderStyle.contentConstraints(); + final bool parentHasDefiniteWidth = + parentRenderStyle.width.isNotAuto || + (parentContentConstraints?.hasTightWidth ?? false); + + bool isStretch = false; + + bool isParentFlex = parentRenderStyle.display == CSSDisplay.flex || + parentRenderStyle.display == CSSDisplay.inlineFlex; + bool isHorizontalDirection = false; + bool isFlexNoWrap = false; + bool isChildStretchSelf = false; + final bool isHorizontalWritingMode = + parentRenderStyle.writingMode == CSSWritingMode.horizontalTb; + if (isParentFlex) { + final bool isPositioned = renderStyle.position == CSSPositionType.absolute || + renderStyle.position == CSSPositionType.fixed; + if (isPositioned) { + return false; + } + + isHorizontalDirection = + CSSFlex.isHorizontalFlexDirection(parentRenderStyle.flexDirection); + isFlexNoWrap = parentRenderStyle.flexWrap != FlexWrap.wrap && + parentRenderStyle.flexWrap != FlexWrap.wrapReverse; + isChildStretchSelf = renderStyle.alignSelf != AlignSelf.auto + ? renderStyle.alignSelf == AlignSelf.stretch + : parentRenderStyle.alignItems == AlignItems.stretch; + } + + CSSLengthValue marginLeft = renderStyle.marginLeft; + CSSLengthValue marginRight = renderStyle.marginRight; + + if (marginLeft.isNotAuto && + marginRight.isNotAuto && + isParentFlex && + isHorizontalWritingMode && + !isHorizontalDirection && + isFlexNoWrap && + isChildStretchSelf && + parentHasDefiniteWidth) { + isStretch = true; + } + + return isStretch; + } + // Max width to constrain its children, used in deciding the line wrapping timing of layout. @override double get contentMaxConstraintsWidth { diff --git a/webf/lib/src/css/text.dart b/webf/lib/src/css/text.dart index a42e499242..9ca27dec9b 100644 --- a/webf/lib/src/css/text.dart +++ b/webf/lib/src/css/text.dart @@ -149,17 +149,49 @@ mixin CSSTextMixin on RenderStyle { } FontWeight? _fontWeight; + int _layoutPassInheritedTextCachePassId = -1; + FontWeight? _layoutPassInheritedFontWeight; + FontStyle? _layoutPassInheritedFontStyle; + CSSLengthValue? _layoutPassInheritedTextIndent; + WordBreak? _layoutPassInheritedWordBreak; + WhiteSpace? _layoutPassInheritedWhiteSpace; + TextDirection? _layoutPassInheritedDirection; + + @pragma('vm:prefer-inline') + bool get _canUseLayoutPassInheritedTextCache => + renderBoxModelInLayoutStack.isNotEmpty; + + @pragma('vm:prefer-inline') + void _ensureLayoutPassInheritedTextCache() { + if (!_canUseLayoutPassInheritedTextCache) { + return; + } + + if (_layoutPassInheritedTextCachePassId != renderBoxModelLayoutPassId) { + _layoutPassInheritedTextCachePassId = renderBoxModelLayoutPassId; + _layoutPassInheritedFontWeight = null; + _layoutPassInheritedFontStyle = null; + _layoutPassInheritedTextIndent = null; + _layoutPassInheritedWordBreak = null; + _layoutPassInheritedWhiteSpace = null; + _layoutPassInheritedDirection = null; + } + } @override FontWeight get fontWeight { - // Get style from self or closest parent if specified style property is not set - // due to style inheritance. - if (_fontWeight == null && parent != null) { - return parent!.fontWeight; + if (_fontWeight != null) return _fontWeight!; + + _ensureLayoutPassInheritedTextCache(); + if (_layoutPassInheritedFontWeight != null) { + return _layoutPassInheritedFontWeight!; } - // The root element has no fontWeight, and the fontWeight is initial. - return _fontWeight ?? FontWeight.w400; + final FontWeight resolved = parent?.fontWeight ?? FontWeight.w400; + if (_canUseLayoutPassInheritedTextCache) { + _layoutPassInheritedFontWeight = resolved; + } + return resolved; } set fontWeight(FontWeight? value) { @@ -172,14 +204,18 @@ mixin CSSTextMixin on RenderStyle { FontStyle? _fontStyle; @override FontStyle get fontStyle { - // Get style from self or closest parent if specified style property is not set - // due to style inheritance. - if (_fontStyle == null && parent != null) { - return parent!.fontStyle; + if (_fontStyle != null) return _fontStyle!; + + _ensureLayoutPassInheritedTextCache(); + if (_layoutPassInheritedFontStyle != null) { + return _layoutPassInheritedFontStyle!; } - // The root element has no fontWeight, and the fontWeight is initial. - return _fontStyle ?? FontStyle.normal; + final FontStyle resolved = parent?.fontStyle ?? FontStyle.normal; + if (_canUseLayoutPassInheritedTextCache) { + _layoutPassInheritedFontStyle = resolved; + } + return resolved; } set fontStyle(FontStyle? value) { @@ -355,10 +391,19 @@ mixin CSSTextMixin on RenderStyle { // text-indent (inherited) CSSLengthValue? _textIndent; CSSLengthValue get textIndent { - if (_textIndent == null && parent != null) { - return (parent as CSSRenderStyle).textIndent; + if (_textIndent != null) return _textIndent!; + + _ensureLayoutPassInheritedTextCache(); + if (_layoutPassInheritedTextIndent != null) { + return _layoutPassInheritedTextIndent!; } - return _textIndent ?? CSSLengthValue.zero; + + final CSSLengthValue resolved = + (parent as CSSRenderStyle?)?.textIndent ?? CSSLengthValue.zero; + if (_canUseLayoutPassInheritedTextCache) { + _layoutPassInheritedTextIndent = resolved; + } + return resolved; } set textIndent(CSSLengthValue? value) { @@ -408,10 +453,18 @@ mixin CSSTextMixin on RenderStyle { WordBreak? _wordBreak; @override WordBreak get wordBreak { - if (_wordBreak == null && parent != null) { - return parent!.wordBreak; + if (_wordBreak != null) return _wordBreak!; + + _ensureLayoutPassInheritedTextCache(); + if (_layoutPassInheritedWordBreak != null) { + return _layoutPassInheritedWordBreak!; } - return _wordBreak ?? WordBreak.normal; + + final WordBreak resolved = parent?.wordBreak ?? WordBreak.normal; + if (_canUseLayoutPassInheritedTextCache) { + _layoutPassInheritedWordBreak = resolved; + } + return resolved; } set wordBreak(WordBreak? value) { @@ -425,12 +478,18 @@ mixin CSSTextMixin on RenderStyle { @override WhiteSpace get whiteSpace { - // Get style from self or closest parent if specified style property is not set - // due to style inheritance. - if (_whiteSpace == null && parent != null) { - return parent!.whiteSpace; + if (_whiteSpace != null) return _whiteSpace!; + + _ensureLayoutPassInheritedTextCache(); + if (_layoutPassInheritedWhiteSpace != null) { + return _layoutPassInheritedWhiteSpace!; + } + + final WhiteSpace resolved = parent?.whiteSpace ?? WhiteSpace.normal; + if (_canUseLayoutPassInheritedTextCache) { + _layoutPassInheritedWhiteSpace = resolved; } - return _whiteSpace ?? WhiteSpace.normal; + return resolved; } set whiteSpace(WhiteSpace? value) { @@ -528,6 +587,13 @@ mixin CSSTextMixin on RenderStyle { dom.Element? _cachedInheritedDirectionParent; void resetInheritedTextCaches() { + _layoutPassInheritedTextCachePassId = -1; + _layoutPassInheritedFontWeight = null; + _layoutPassInheritedFontStyle = null; + _layoutPassInheritedTextIndent = null; + _layoutPassInheritedWordBreak = null; + _layoutPassInheritedWhiteSpace = null; + _layoutPassInheritedDirection = null; _cachedInheritedTextAlign = null; _cachedInheritedTextAlignParent = null; _cachedInheritedDirection = null; @@ -564,9 +630,16 @@ mixin CSSTextMixin on RenderStyle { // render reparenting (e.g., positioned elements), prefer the DOM parent’s // renderStyle over the render tree parent to ensure correct inheritance. if (_direction != null) return _direction!; + _ensureLayoutPassInheritedTextCache(); + if (_layoutPassInheritedDirection != null) { + return _layoutPassInheritedDirection!; + } final dom.Element? domParent = target.parentElement; if (_cachedInheritedDirection != null && identical(_cachedInheritedDirectionParent, domParent)) { + if (_canUseLayoutPassInheritedTextCache) { + _layoutPassInheritedDirection = _cachedInheritedDirection; + } return _cachedInheritedDirection!; } final TextDirection resolved; @@ -579,6 +652,9 @@ mixin CSSTextMixin on RenderStyle { } _cachedInheritedDirectionParent = domParent; _cachedInheritedDirection = resolved; + if (_canUseLayoutPassInheritedTextCache) { + _layoutPassInheritedDirection = resolved; + } return resolved; } diff --git a/webf/lib/src/css/values/length.dart b/webf/lib/src/css/values/length.dart index b4b97a10de..a0ebe69fdb 100644 --- a/webf/lib/src/css/values/length.dart +++ b/webf/lib/src/css/values/length.dart @@ -247,6 +247,11 @@ class CSSLengthValue { RenderStyle? renderStyle; String? propertyName; double? _computedValue; + int _layoutPassComputedValuePassId = -1; + RenderStyle? _layoutPassComputedValueStyle; + double? _layoutPassComputedValue; + String? _layoutPassComputedValuePropertyName; + double? _layoutPassDependencyScalar; static bool _isPercentageRelativeContainerRenderStyle(RenderStyle renderStyle) { // Use effectiveDisplay so that blockification/inlinification is respected @@ -305,10 +310,14 @@ class CSSLengthValue { final FlexDirection dir = flexParent.flexDirection; final bool parentIsColumn = dir == FlexDirection.column || dir == FlexDirection.columnReverse; if (parentIsColumn) { + if (flexParent.writingMode != CSSWritingMode.horizontalTb) { + return false; + } final AlignSelf self = rs.alignSelf; final bool stretched = self == AlignSelf.stretch || (self == AlignSelf.auto && flexParent.alignItems == AlignItems.stretch); if (!stretched) return false; + return true; } else { // Row-direction shrink-to-fit flex item: treat as indefinite for percentage resolution. return false; @@ -325,23 +334,98 @@ class CSSLengthValue { return false; } + double? _getCalcLayoutPassDependency( + RenderStyle currentRenderStyle, String? realPropertyName) { + if (realPropertyName != WIDTH && + realPropertyName != MIN_WIDTH && + realPropertyName != MAX_WIDTH) { + return null; + } + + final CSSPositionType positionType = currentRenderStyle.position; + final bool isPositioned = + positionType == CSSPositionType.absolute || + positionType == CSSPositionType.fixed; + + RenderStyle? parentRenderStyle = isPositioned + ? currentRenderStyle.target.getContainingBlockElement()?.renderStyle + : currentRenderStyle.getAttachedRenderParentRenderStyle(); + + while (parentRenderStyle != null) { + if (parentRenderStyle.isBoxModel() && + _isPercentageRelativeContainerRenderStyle(parentRenderStyle)) { + break; + } + parentRenderStyle = parentRenderStyle.getAttachedRenderParentRenderStyle(); + } + + return isPositioned + ? (parentRenderStyle?.paddingBoxLogicalWidth ?? + parentRenderStyle?.paddingBoxWidth) + : (parentRenderStyle?.contentBoxLogicalWidth ?? + parentRenderStyle?.contentBoxWidth); + } + // Note return value of double.infinity means the value is resolved as the initial value // which can not be computed to a specific value, eg. percentage height is sometimes parsed // to be auto due to parent height not defined. double get computedValue { + final RenderStyle? currentRenderStyle = renderStyle; + final String? currentPropertyName = propertyName; + final String? realPropertyName = _realPropertyName; + final bool canUseLayoutPassLocalCache = + calcValue == null && + currentRenderStyle?.hasRenderBox() == true && + currentPropertyName != null && + type != CSSLengthType.PERCENTAGE && + renderBoxModelInLayoutStack.isNotEmpty; + final double? calcDependencyScalar = + calcValue != null && currentRenderStyle != null + ? _getCalcLayoutPassDependency(currentRenderStyle, realPropertyName) + : null; + final bool canUseLayoutPassCalcCache = + calcValue != null && + currentRenderStyle?.hasRenderBox() == true && + currentPropertyName != null && + calcDependencyScalar != null && + renderBoxModelInLayoutStack.isNotEmpty; + + if ((canUseLayoutPassLocalCache || canUseLayoutPassCalcCache) && + _layoutPassComputedValuePassId == renderBoxModelLayoutPassId && + identical(_layoutPassComputedValueStyle, currentRenderStyle) && + _layoutPassComputedValuePropertyName == currentPropertyName && + (!canUseLayoutPassCalcCache || + _layoutPassDependencyScalar == calcDependencyScalar)) { + final double? cachedPassValue = _layoutPassComputedValue; + if (cachedPassValue != null) { + return cachedPassValue; + } + } + if (calcValue == null && type == CSSLengthType.PX) { _computedValue = value ?? 0; + if (canUseLayoutPassLocalCache) { + _layoutPassComputedValuePassId = renderBoxModelLayoutPassId; + _layoutPassComputedValueStyle = currentRenderStyle; + _layoutPassComputedValue = _computedValue; + _layoutPassComputedValuePropertyName = currentPropertyName; + _layoutPassDependencyScalar = null; + } return _computedValue!; } if (calcValue != null) { _computedValue = calcValue!.computedValue(propertyName ?? '') ?? 0; + if (canUseLayoutPassCalcCache) { + _layoutPassComputedValuePassId = renderBoxModelLayoutPassId; + _layoutPassComputedValueStyle = currentRenderStyle; + _layoutPassComputedValue = _computedValue; + _layoutPassComputedValuePropertyName = currentPropertyName; + _layoutPassDependencyScalar = calcDependencyScalar; + } return _computedValue!; } - final RenderStyle? currentRenderStyle = renderStyle; - final String? currentPropertyName = propertyName; - // Use cached value if type is not percentage which may needs 2 layout passes to resolve the // final computed value. if (currentRenderStyle?.hasRenderBox() == true && @@ -358,7 +442,6 @@ class CSSLengthValue { return attachedParentRenderStyle ??= currentRenderStyle?.getAttachedRenderParentRenderStyle(); } - final String? realPropertyName = _realPropertyName; switch (type) { case CSSLengthType.PX: _computedValue = value; @@ -1008,14 +1091,26 @@ class CSSLengthValue { if (currentRenderStyle?.hasRenderBox() == true && currentPropertyName != null && type != CSSLengthType.PERCENTAGE) { cacheComputedValue(currentRenderStyle!, currentPropertyName, _computedValue!); } + if (canUseLayoutPassLocalCache) { + _layoutPassComputedValuePassId = renderBoxModelLayoutPassId; + _layoutPassComputedValueStyle = currentRenderStyle; + _layoutPassComputedValue = _computedValue; + _layoutPassComputedValuePropertyName = currentPropertyName; + _layoutPassDependencyScalar = null; + } return _computedValue!; } bool get isAuto { + if (type == CSSLengthType.AUTO) { + return true; + } if (calcValue != null) { if (calcValue!.expression == null) { return true; } + } else if (type != CSSLengthType.PERCENTAGE) { + return false; } switch (propertyName) { // Length is considered as auto of following properties @@ -1041,7 +1136,7 @@ class CSSLengthValue { } break; } - return type == CSSLengthType.AUTO; + return false; } bool get isNotAuto { diff --git a/webf/lib/src/rendering/box_model.dart b/webf/lib/src/rendering/box_model.dart index 0734b69483..31db46aabc 100644 --- a/webf/lib/src/rendering/box_model.dart +++ b/webf/lib/src/rendering/box_model.dart @@ -148,6 +148,9 @@ abstract class RenderBoxModel extends RenderBox double? computeCssLastBaselineOf(TextBaseline baseline) => _cssLastBaseline; + int _cachedWidgetElementChildLayoutPassId = -1; + RenderWidgetElementChild? _cachedWidgetElementChild; + // Utilities for children to update baseline caches during their own layout. @protected void setCssBaselines({double? first, double? last}) { @@ -512,21 +515,12 @@ abstract class RenderBoxModel extends RenderBox style.getAttachedRenderParentRenderStyle(); final RenderBoxModel? attachedParentRenderBoxModel = attachedParentStyle?.attachedRenderBoxModel; + final EdgeInsets stylePadding = style.padding; + final EdgeInsets styleBorder = style.border; CSSDisplay? effectiveDisplay = style.effectiveDisplay; bool isDisplayInline = effectiveDisplay == CSSDisplay.inline; - double? minWidth = - style.minWidth.isAuto ? null : style.minWidth.computedValue; - double? maxWidth = - style.maxWidth.isNone ? null : style.maxWidth.computedValue; - double? minHeight = style.minHeight.isAuto - ? null - : style.minHeight.computedValue; - double? maxHeight = style.maxHeight.isNone - ? null - : style.maxHeight.computedValue; - // Need to calculated logic content size on every layout. style.computeContentBoxLogicalWidth(); style.computeContentBoxLogicalHeight(); @@ -534,10 +528,7 @@ abstract class RenderBoxModel extends RenderBox // Width should be not smaller than border and padding in horizontal direction // when box-sizing is border-box which is only supported. double minConstraintWidth = - style.effectiveBorderLeftWidth.computedValue + - style.effectiveBorderRightWidth.computedValue + - style.paddingLeft.computedValue + - style.paddingRight.computedValue; + styleBorder.horizontal + stylePadding.horizontal; double? parentBoxContentConstraintsWidth; if (style.isParentRenderBoxModel() && @@ -681,9 +672,34 @@ abstract class RenderBoxModel extends RenderBox !style.isSelfRenderReplaced() && style.borderBoxLogicalWidth == null && parentBoxContentConstraintsWidth != null) { - parentBoxContentConstraintsWidth = null; + parentBoxContentConstraintsWidth = null; } + double? containingBlockPaddingBoxWidth; + if (absOrFixedForWidth && + widthAutoForAbs && + style.left.isNotAuto && + style.right.isNotAuto && + parent is RenderBoxModel) { + final RenderBoxModel cb = parent as RenderBoxModel; + final BoxConstraints? pcc = cb.contentConstraints; + if (pcc != null && pcc.maxWidth.isFinite) { + containingBlockPaddingBoxWidth = + pcc.maxWidth + cb.renderStyle.padding.horizontal; + } + } + + double? minWidth = + style.minWidth.isAuto ? null : style.minWidth.computedValue; + double? maxWidth = + style.maxWidth.isNone ? null : style.maxWidth.computedValue; + double? minHeight = style.minHeight.isAuto + ? null + : style.minHeight.computedValue; + double? maxHeight = style.maxHeight.isNone + ? null + : style.maxHeight.computedValue; + double maxConstraintWidth = style.borderBoxLogicalWidth ?? parentBoxContentConstraintsWidth ?? double.infinity; @@ -703,17 +719,7 @@ abstract class RenderBoxModel extends RenderBox style.left.isNotAuto && style.right.isNotAuto && parent is RenderBoxModel) { - final RenderBoxModel cb = parent as RenderBoxModel; - double? parentPaddingBoxWidth; - // Use the parent's content constraints (plus padding) as the containing block width. - // Avoid using parent.size or style-tree logical widths here to prevent feedback loops - // in flex/inline-block shrink-to-fit scenarios. - final BoxConstraints? pcc = cb.contentConstraints; - if (pcc != null && pcc.maxWidth.isFinite) { - parentPaddingBoxWidth = pcc.maxWidth + - cb.renderStyle.paddingLeft.computedValue + - cb.renderStyle.paddingRight.computedValue; - } + final double? parentPaddingBoxWidth = containingBlockPaddingBoxWidth; if (parentPaddingBoxWidth != null && parentPaddingBoxWidth.isFinite) { // Solve the horizontal insets equation for the child border-box width. double solvedBorderBoxWidth = parentPaddingBoxWidth - @@ -789,10 +795,7 @@ abstract class RenderBoxModel extends RenderBox // Height should be not smaller than border and padding in vertical direction // when box-sizing is border-box which is only supported. double minConstraintHeight = - style.effectiveBorderTopWidth.computedValue + - style.effectiveBorderBottomWidth.computedValue + - style.paddingTop.computedValue + - style.paddingBottom.computedValue; + styleBorder.vertical + stylePadding.vertical; double maxConstraintHeight = style.borderBoxLogicalHeight ?? double.infinity; @@ -836,10 +839,7 @@ abstract class RenderBoxModel extends RenderBox maxConstraintWidth > maxWidth ? maxWidth : maxConstraintWidth; // Only reduce minConstraintWidth if maxWidth is larger than border+padding requirements double borderPadding = - renderStyle.effectiveBorderLeftWidth.computedValue + - renderStyle.effectiveBorderRightWidth.computedValue + - renderStyle.paddingLeft.computedValue + - renderStyle.paddingRight.computedValue; + styleBorder.horizontal + stylePadding.horizontal; if (maxWidth >= borderPadding) { minConstraintWidth = minConstraintWidth > maxWidth ? maxWidth : minConstraintWidth; @@ -1126,11 +1126,27 @@ abstract class RenderBoxModel extends RenderBox /// This is used to access parent constraints for layout calculations, allowing HTML elements /// to be aware of their Flutter widget container constraints for proper sizing and layout. RenderWidgetElementChild? findWidgetElementChild() { + if (renderBoxModelInLayoutStack.isNotEmpty && + _cachedWidgetElementChildLayoutPassId == renderBoxModelLayoutPassId) { + return _cachedWidgetElementChild; + } + RenderObject? ancestor = parent; while (ancestor != null) { - if (ancestor is RenderWidgetElementChild) return ancestor; + if (ancestor is RenderWidgetElementChild) { + if (renderBoxModelInLayoutStack.isNotEmpty) { + _cachedWidgetElementChildLayoutPassId = renderBoxModelLayoutPassId; + _cachedWidgetElementChild = ancestor; + } + return ancestor; + } ancestor = ancestor.parent; } + + if (renderBoxModelInLayoutStack.isNotEmpty) { + _cachedWidgetElementChildLayoutPassId = renderBoxModelLayoutPassId; + _cachedWidgetElementChild = null; + } return null; } diff --git a/webf/lib/src/rendering/flex.dart b/webf/lib/src/rendering/flex.dart index c969805ead..a5ca2062ba 100644 --- a/webf/lib/src/rendering/flex.dart +++ b/webf/lib/src/rendering/flex.dart @@ -20,6 +20,10 @@ import 'package:webf/src/html/semantics_text.dart' show SpanElement; import 'package:webf/src/html/text.dart'; import 'package:webf/widget.dart'; +const bool _enableFlexProfileSections = + !kReleaseMode && + bool.fromEnvironment('WEBF_ENABLE_LAYOUT_PROFILE_SECTIONS'); + enum _FlexFastPathRejectReason { verticalDirection, wrappedContainer, @@ -33,6 +37,10 @@ enum _FlexFastPathRejectReason { wouldShrink, } +final Map + _sharedPlainTextFlexIntrinsicMeasurementCache = + {}; + String _flexFastPathRejectReasonLabel(_FlexFastPathRejectReason reason) { switch (reason) { case _FlexFastPathRejectReason.verticalDirection: @@ -650,11 +658,15 @@ class _FlexIntrinsicMeasurementLookupResult { this.entry, this.missReason, this.missDetails, + this.flowChild, + this.reusableStateSignature, }); final _FlexIntrinsicMeasurementCacheEntry? entry; final _FlexAnonymousMetricsMissReason? missReason; final Map? missDetails; + final RenderFlowLayout? flowChild; + final int? reusableStateSignature; } // Position and size info of each run (flex line) in flex layout. @@ -738,6 +750,8 @@ class _RunChild { required this.marginBottomAuto, required this.isReplaced, required this.aspectRatio, + required this.mainAxisExtentAdjustment, + required this.crossAxisExtentAdjustment, }) : _child = child, _originalMainSize = originalMainSize, @@ -819,6 +833,10 @@ class _RunChild { final bool marginBottomAuto; final bool isReplaced; final double? aspectRatio; + final double mainAxisExtentAdjustment; + final double crossAxisExtentAdjustment; + double? cachedMinMainAxisSize; + double? cachedMaxMainAxisSize; } class _OrderedFlexItem { @@ -857,6 +875,32 @@ class _FlexResolutionInputs { final bool isMainSizeDefinite; } +class _AdjustedConstraintsCacheEntry { + const _AdjustedConstraintsCacheEntry({ + required this.oldConstraints, + required this.childFlexedMainSize, + required this.childStretchedCrossSize, + required this.lineChildrenCount, + required this.preserveMainAxisSize, + required this.hasOverrideContentLogicalWidth, + required this.hasOverrideContentLogicalHeight, + required this.contentBoxLogicalWidth, + required this.contentBoxLogicalHeight, + required this.constraints, + }); + + final BoxConstraints oldConstraints; + final double? childFlexedMainSize; + final double? childStretchedCrossSize; + final int lineChildrenCount; + final double? preserveMainAxisSize; + final bool hasOverrideContentLogicalWidth; + final bool hasOverrideContentLogicalHeight; + final double? contentBoxLogicalWidth; + final double? contentBoxLogicalHeight; + final BoxConstraints constraints; +} + class _FlexContainerInvariants { const _FlexContainerInvariants({ required this.isHorizontalFlexDirection, @@ -1178,16 +1222,24 @@ class RenderFlexLayout extends RenderLayoutBox { // Cache the intrinsic size of children before flex-grow/flex-shrink // to avoid relayout when style of flex items changes. - Expando _childrenIntrinsicMainSizes = Expando('childrenIntrinsicMainSizes'); + final Map _childrenIntrinsicMainSizes = + Map.identity(); // Cache original constraints of children on the first layout. Expando _childrenOldConstraints = Expando('childrenOldConstraints'); Expando<_FlexIntrinsicMeasurementCacheBucket> _childrenIntrinsicMeasureCache = Expando<_FlexIntrinsicMeasurementCacheBucket>('childrenIntrinsicMeasureCache'); - Expando _childrenRequirePostMeasureLayout = - Expando('childrenRequirePostMeasureLayout'); - Expando? _transientChildSizeOverrides; - Expando? _metricsOnlyIntrinsicMeasureChildEligibilityCache; + final Map _childrenRequirePostMeasureLayout = + Map.identity(); + int _adjustedConstraintsCachePassId = -1; + final Map + _adjustedConstraintsCache = + Map.identity(); + Map? _transientChildSizeOverrides; + Map? _metricsOnlyIntrinsicMeasureChildEligibilityCache; + int _reusableIntrinsicStyleSignaturePassId = -1; + final Map _cachedReusableIntrinsicStyleSignatures = + Map.identity(); _FlexContainerInvariants? _layoutInvariants; @@ -1197,14 +1249,17 @@ class RenderFlexLayout extends RenderLayoutBox { // Do not forget to clear reference variables, or it will cause memory leaks! _flexLineBoxMetrics.clear(); - _childrenIntrinsicMainSizes = Expando('childrenIntrinsicMainSizes'); + _childrenIntrinsicMainSizes.clear(); _childrenOldConstraints = Expando('childrenOldConstraints'); _childrenIntrinsicMeasureCache = Expando<_FlexIntrinsicMeasurementCacheBucket>('childrenIntrinsicMeasureCache'); - _childrenRequirePostMeasureLayout = - Expando('childrenRequirePostMeasureLayout'); + _childrenRequirePostMeasureLayout.clear(); + _adjustedConstraintsCachePassId = -1; + _adjustedConstraintsCache.clear(); _transientChildSizeOverrides = null; _metricsOnlyIntrinsicMeasureChildEligibilityCache = null; + _reusableIntrinsicStyleSignaturePassId = -1; + _cachedReusableIntrinsicStyleSignatures.clear(); } @override @@ -1645,6 +1700,42 @@ class RenderFlexLayout extends RenderLayoutBox { return minMainSize ?? 0; } + @pragma('vm:prefer-inline') + double _getRunChildMinMainAxisSize(_RunChild runChild) { + final double? cached = runChild.cachedMinMainAxisSize; + if (cached != null) { + return cached; + } + + final RenderBoxModel? effectiveChild = runChild.effectiveChild; + if (effectiveChild == null) { + runChild.cachedMinMainAxisSize = 0; + return 0; + } + + final double resolved = _getMinMainAxisSize(effectiveChild); + runChild.cachedMinMainAxisSize = resolved; + return resolved; + } + + @pragma('vm:prefer-inline') + double _getRunChildMaxMainAxisSize(_RunChild runChild) { + final double? cached = runChild.cachedMaxMainAxisSize; + if (cached != null) { + return cached; + } + + final RenderBoxModel? effectiveChild = runChild.effectiveChild; + if (effectiveChild == null) { + runChild.cachedMaxMainAxisSize = double.infinity; + return double.infinity; + } + + final double resolved = _getMaxMainAxisSize(effectiveChild); + runChild.cachedMaxMainAxisSize = resolved; + return resolved; + } + // Find and mark elements that only contain text boxes for flex relayout optimization. // This enables text boxes to use infinite width during flex layout to expand naturally, // preventing box constraint errors when flex items are resized. @@ -2174,38 +2265,6 @@ class RenderFlexLayout extends RenderLayoutBox { } } - double _getCrossAxisExtent(RenderBox? child) { - double marginHorizontal = 0; - double marginVertical = 0; - - RenderBoxModel? childRenderBoxModel; - if (child is RenderBoxModel) { - childRenderBoxModel = child; - } else if (child is RenderPositionPlaceholder) { - // Position placeholder of flex item need to layout as its original renderBox - // so it needs to add margin to its extent. - childRenderBoxModel = child.positioned; - } - - if (childRenderBoxModel != null) { - marginHorizontal = childRenderBoxModel.renderStyle.marginLeft.computedValue + - childRenderBoxModel.renderStyle.marginRight.computedValue; - marginVertical = childRenderBoxModel.renderStyle.marginTop.computedValue + - childRenderBoxModel.renderStyle.marginBottom.computedValue; - } - - Size? childSize = _getChildSize(child); - - if (_isHorizontalFlexDirection) { - return childSize!.height + marginVertical; - } else { - if (child is RenderLayoutBox && child.isNegativeMarginChangeHSize) { - return _horizontalMarginNegativeSet(childSize!.width, child); - } - return childSize!.width + marginHorizontal; - } - } - double _horizontalMarginNegativeSet(double baseSize, RenderBoxModel box, {bool isHorizontal = false}) { CSSRenderStyle boxStyle = box.renderStyle; double? marginLeft = boxStyle.marginLeft.computedValue; @@ -2228,37 +2287,6 @@ class RenderFlexLayout extends RenderLayoutBox { return baseSize + box.renderStyle.margin.vertical; } - double _getMainAxisExtent(RenderBox child, {bool shouldUseIntrinsicMainSize = false}) { - double marginHorizontal = 0; - double marginVertical = 0; - - RenderBoxModel? childRenderBoxModel; - if (child is RenderBoxModel) { - childRenderBoxModel = child; - } else if (child is RenderPositionPlaceholder) { - // Position placeholder of flex item need to layout as its original renderBox - // so it needs to add margin to its extent. - childRenderBoxModel = child.positioned; - } - - if (childRenderBoxModel != null) { - marginHorizontal = childRenderBoxModel.renderStyle.marginLeft.computedValue + - childRenderBoxModel.renderStyle.marginRight.computedValue; - marginVertical = childRenderBoxModel.renderStyle.marginTop.computedValue + - childRenderBoxModel.renderStyle.marginBottom.computedValue; - } - - double baseSize = _getMainSize(child, shouldUseIntrinsicMainSize: shouldUseIntrinsicMainSize); - if (_isHorizontalFlexDirection) { - if (child is RenderLayoutBox && child.isNegativeMarginChangeHSize) { - return _horizontalMarginNegativeSet(baseSize, child); - } - return baseSize + marginHorizontal; - } else { - return baseSize + marginVertical; - } - } - double _getMainSize(RenderBox child, {bool shouldUseIntrinsicMainSize = false}) { Size? childSize = _getChildSize(child, shouldUseIntrinsicMainSize: shouldUseIntrinsicMainSize); if (_isHorizontalFlexDirection) { @@ -2390,16 +2418,20 @@ class RenderFlexLayout extends RenderLayoutBox { _RunChild _createRunChildMetadata(RenderBox child, double originalMainSize, {required RenderBoxModel? effectiveChild, required double? usedFlexBasis}) { + final RenderBoxModel? marginBoxModel = + child is RenderBoxModel ? child : (child is RenderPositionPlaceholder ? child.positioned : null); + double marginHorizontal = 0.0; + double marginVertical = 0.0; double mainAxisMargin = 0.0; - if (effectiveChild != null) { - final RenderStyle s = effectiveChild.renderStyle; - final double marginHorizontal = s.marginLeft.computedValue + s.marginRight.computedValue; - final double marginVertical = s.marginTop.computedValue + s.marginBottom.computedValue; + final RenderStyle? marginBoxStyle = marginBoxModel?.renderStyle; + if (marginBoxStyle != null) { + final RenderStyle s = marginBoxStyle; + marginHorizontal = + s.marginLeft.computedValue + s.marginRight.computedValue; + marginVertical = s.marginTop.computedValue + s.marginBottom.computedValue; mainAxisMargin = _isHorizontalFlexDirection ? marginHorizontal : marginVertical; } - final RenderBoxModel? marginBoxModel = - child is RenderBoxModel ? child : (child is RenderPositionPlaceholder ? child.positioned : null); final RenderStyle? marginStyle = marginBoxModel?.renderStyle; final bool marginLeftAuto = marginStyle?.marginLeft.isAuto ?? false; final bool marginRightAuto = marginStyle?.marginRight.isAuto ?? false; @@ -2414,6 +2446,31 @@ class RenderFlexLayout extends RenderLayoutBox { final double flexGrow = _getFlexGrow(child); final double flexShrink = _getFlexShrink(child); + final double mainAxisExtentAdjustment; + final double crossAxisExtentAdjustment; + if (_isHorizontalFlexDirection) { + if (marginBoxModel is RenderLayoutBox && + marginBoxModel.isNegativeMarginChangeHSize) { + mainAxisExtentAdjustment = _horizontalMarginNegativeSet( + 0, + marginBoxModel, + ); + } else { + mainAxisExtentAdjustment = marginHorizontal; + } + crossAxisExtentAdjustment = marginVertical; + } else { + mainAxisExtentAdjustment = marginVertical; + if (marginBoxModel is RenderLayoutBox && + marginBoxModel.isNegativeMarginChangeHSize) { + crossAxisExtentAdjustment = _horizontalMarginNegativeSet( + 0, + marginBoxModel, + ); + } else { + crossAxisExtentAdjustment = marginHorizontal; + } + } return _RunChild( child, @@ -2438,9 +2495,36 @@ class RenderFlexLayout extends RenderLayoutBox { marginBottomAuto: marginBottomAuto, isReplaced: effectiveChild?.renderStyle.isSelfRenderReplaced() ?? false, aspectRatio: effectiveChild?.renderStyle.aspectRatio, + mainAxisExtentAdjustment: mainAxisExtentAdjustment, + crossAxisExtentAdjustment: crossAxisExtentAdjustment, ); } + double _getRunChildMainSize(_RunChild runChild, + {bool shouldUseIntrinsicMainSize = false}) { + final Size childSize = _getChildSize( + runChild.child, + shouldUseIntrinsicMainSize: shouldUseIntrinsicMainSize, + )!; + return _isHorizontalFlexDirection ? childSize.width : childSize.height; + } + + double _getRunChildMainAxisExtent(_RunChild runChild, + {bool shouldUseIntrinsicMainSize = false}) { + return _getRunChildMainSize( + runChild, + shouldUseIntrinsicMainSize: shouldUseIntrinsicMainSize, + ) + + runChild.mainAxisExtentAdjustment; + } + + double _getRunChildCrossAxisExtent(_RunChild runChild) { + final Size childSize = _getChildSize(runChild.child)!; + final double crossSize = + _isHorizontalFlexDirection ? childSize.height : childSize.width; + return crossSize + runChild.crossAxisExtentAdjustment; + } + void _cacheOriginalConstraintsIfNeeded(RenderBox child, BoxConstraints appliedConstraints) { RenderBoxModel? box = child is RenderBoxModel ? child @@ -2721,40 +2805,23 @@ class RenderFlexLayout extends RenderLayoutBox { return (value * 100).round(); } - int? _computeReusableIntrinsicMeasurementStateSignature( - RenderBox child, - BoxConstraints childConstraints, { - RenderFlowLayout? flowChild, - }) { - final RenderFlowLayout? effectiveFlowChild = - flowChild ?? - _getCacheableIntrinsicMeasureFlowChild( - child, - allowAnonymous: true, - ); - if (effectiveFlowChild == null) { - return null; + int _computeReusableIntrinsicMeasurementStyleSignature( + CSSRenderStyle style) { + if (renderBoxModelInLayoutStack.isNotEmpty) { + final int currentLayoutPassId = renderBoxModelLayoutPassId; + if (_reusableIntrinsicStyleSignaturePassId != currentLayoutPassId) { + _reusableIntrinsicStyleSignaturePassId = currentLayoutPassId; + _cachedReusableIntrinsicStyleSignatures.clear(); + } else { + final int? cachedSignature = + _cachedReusableIntrinsicStyleSignatures[style]; + if (cachedSignature != null) { + return cachedSignature; + } + } } int hash = 0; - final CSSRenderStyle style = effectiveFlowChild.renderStyle; - hash = _hashReusableIntrinsicMeasurementState(hash, effectiveFlowChild.hashCode); - hash = _hashReusableIntrinsicMeasurementState( - hash, - _quantizeReusableIntrinsicMeasurementDouble(childConstraints.minWidth), - ); - hash = _hashReusableIntrinsicMeasurementState( - hash, - _quantizeReusableIntrinsicMeasurementDouble(childConstraints.maxWidth), - ); - hash = _hashReusableIntrinsicMeasurementState( - hash, - _quantizeReusableIntrinsicMeasurementDouble(childConstraints.minHeight), - ); - hash = _hashReusableIntrinsicMeasurementState( - hash, - _quantizeReusableIntrinsicMeasurementDouble(childConstraints.maxHeight), - ); hash = _hashReusableIntrinsicMeasurementState(hash, style.display.hashCode); hash = _hashReusableIntrinsicMeasurementState(hash, style.position.hashCode); hash = _hashReusableIntrinsicMeasurementState(hash, style.whiteSpace.hashCode); @@ -2822,6 +2889,51 @@ class RenderFlexLayout extends RenderLayoutBox { style.flexBasis!.type.hashCode, ); } + final int signature = _finishReusableIntrinsicMeasurementState(hash); + if (renderBoxModelInLayoutStack.isNotEmpty) { + _cachedReusableIntrinsicStyleSignatures[style] = signature; + } + return signature; + } + + int? _computeReusableIntrinsicMeasurementStateSignature( + RenderBox child, + BoxConstraints childConstraints, { + RenderFlowLayout? flowChild, + }) { + final RenderFlowLayout? effectiveFlowChild = + flowChild ?? + _getCacheableIntrinsicMeasureFlowChild( + child, + allowAnonymous: true, + ); + if (effectiveFlowChild == null) { + return null; + } + + int hash = 0; + final CSSRenderStyle style = effectiveFlowChild.renderStyle; + hash = _hashReusableIntrinsicMeasurementState(hash, effectiveFlowChild.hashCode); + hash = _hashReusableIntrinsicMeasurementState( + hash, + _quantizeReusableIntrinsicMeasurementDouble(childConstraints.minWidth), + ); + hash = _hashReusableIntrinsicMeasurementState( + hash, + _quantizeReusableIntrinsicMeasurementDouble(childConstraints.maxWidth), + ); + hash = _hashReusableIntrinsicMeasurementState( + hash, + _quantizeReusableIntrinsicMeasurementDouble(childConstraints.minHeight), + ); + hash = _hashReusableIntrinsicMeasurementState( + hash, + _quantizeReusableIntrinsicMeasurementDouble(childConstraints.maxHeight), + ); + hash = _hashReusableIntrinsicMeasurementState( + hash, + _computeReusableIntrinsicMeasurementStyleSignature(style), + ); final InlineFormattingContext? ifc = effectiveFlowChild.inlineFormattingContext; if (ifc != null) { hash = _hashReusableIntrinsicMeasurementState( @@ -2832,6 +2944,50 @@ class RenderFlexLayout extends RenderLayoutBox { return _finishReusableIntrinsicMeasurementState(hash); } + int? _computePlainTextSharedIntrinsicMeasurementSignature( + RenderBox child, + BoxConstraints childConstraints, { + RenderFlowLayout? flowChild, + }) { + final RenderFlowLayout? effectiveFlowChild = + flowChild ?? + _getCacheableIntrinsicMeasureFlowChild( + child, + allowAnonymous: true, + ); + if (effectiveFlowChild == null) { + return null; + } + + final InlineFormattingContext? ifc = effectiveFlowChild.inlineFormattingContext; + if (ifc == null || !ifc.isPlainTextOnlyForSharedReuse) { + return null; + } + + int hash = 0; + hash = _hashReusableIntrinsicMeasurementState( + hash, + _quantizeReusableIntrinsicMeasurementDouble(childConstraints.minWidth), + ); + hash = _hashReusableIntrinsicMeasurementState( + hash, + _quantizeReusableIntrinsicMeasurementDouble(childConstraints.maxWidth), + ); + hash = _hashReusableIntrinsicMeasurementState( + hash, + _quantizeReusableIntrinsicMeasurementDouble(childConstraints.minHeight), + ); + hash = _hashReusableIntrinsicMeasurementState( + hash, + _quantizeReusableIntrinsicMeasurementDouble(childConstraints.maxHeight), + ); + hash = _hashReusableIntrinsicMeasurementState( + hash, + ifc.plainTextSharedReuseSignature(), + ); + return _finishReusableIntrinsicMeasurementState(hash); + } + _FlexIntrinsicMeasurementLookupResult _lookupReusableIntrinsicMeasurement( RenderBox child, BoxConstraints childConstraints, @@ -2858,17 +3014,55 @@ class RenderFlexLayout extends RenderLayoutBox { if (flowChild == null) { return const _FlexIntrinsicMeasurementLookupResult(); } + final int? sharedPlainTextSignature = + _computePlainTextSharedIntrinsicMeasurementSignature( + child, + childConstraints, + flowChild: flowChild, + ); final _FlexIntrinsicMeasurementCacheBucket? cacheBucket = _childrenIntrinsicMeasureCache[child]; if (cacheBucket == null || cacheBucket.entries.isEmpty) { - return const _FlexIntrinsicMeasurementLookupResult( + final _FlexIntrinsicMeasurementCacheEntry? sharedEntry = + sharedPlainTextSignature == null + ? null + : _sharedPlainTextFlexIntrinsicMeasurementCache[ + sharedPlainTextSignature + ]; + if (sharedEntry != null) { + final _FlexIntrinsicMeasurementCacheBucket bucket = + _childrenIntrinsicMeasureCache[child] ?? + _FlexIntrinsicMeasurementCacheBucket(); + bucket.store(sharedEntry); + _childrenIntrinsicMeasureCache[child] = bucket; + return _FlexIntrinsicMeasurementLookupResult( + entry: sharedEntry, + flowChild: flowChild, + ); + } + return _FlexIntrinsicMeasurementLookupResult( + flowChild: flowChild, missReason: _FlexAnonymousMetricsMissReason.missingCacheEntry, ); } final _FlexIntrinsicMeasurementCacheEntry? cacheEntry = cacheBucket.lookupLatest(childConstraints); if (cacheEntry == null) { - return const _FlexIntrinsicMeasurementLookupResult( + final _FlexIntrinsicMeasurementCacheEntry? sharedEntry = + sharedPlainTextSignature == null + ? null + : _sharedPlainTextFlexIntrinsicMeasurementCache[ + sharedPlainTextSignature + ]; + if (sharedEntry != null) { + cacheBucket.store(sharedEntry); + return _FlexIntrinsicMeasurementLookupResult( + entry: sharedEntry, + flowChild: flowChild, + ); + } + return _FlexIntrinsicMeasurementLookupResult( + flowChild: flowChild, missReason: _FlexAnonymousMetricsMissReason.constraintsMismatch, ); } @@ -2887,19 +3081,29 @@ class RenderFlexLayout extends RenderLayoutBox { final _FlexIntrinsicMeasurementCacheEntry? reusableEntry = cacheBucket.lookupReusable(childConstraints, reusableStateSignature); if (reusableEntry != null) { - return _FlexIntrinsicMeasurementLookupResult(entry: reusableEntry); + return _FlexIntrinsicMeasurementLookupResult( + entry: reusableEntry, + flowChild: flowChild, + reusableStateSignature: reusableStateSignature, + ); } if (flowNeedsRelayout) { - return const _FlexIntrinsicMeasurementLookupResult( + return _FlexIntrinsicMeasurementLookupResult( + flowChild: flowChild, + reusableStateSignature: reusableStateSignature, missReason: _FlexAnonymousMetricsMissReason.flowNeedsRelayout, ); } if (childNeedsRelayout) { - return const _FlexIntrinsicMeasurementLookupResult( + return _FlexIntrinsicMeasurementLookupResult( + flowChild: flowChild, + reusableStateSignature: reusableStateSignature, missReason: _FlexAnonymousMetricsMissReason.childNeedsRelayout, ); } return _FlexIntrinsicMeasurementLookupResult( + flowChild: flowChild, + reusableStateSignature: reusableStateSignature, missReason: reusableStateSignature == null ? _FlexAnonymousMetricsMissReason.subtreeIntrinsicDirty : _FlexAnonymousMetricsMissReason.reusableStateMismatch, @@ -2908,7 +3112,10 @@ class RenderFlexLayout extends RenderLayoutBox { : null, ); } - return _FlexIntrinsicMeasurementLookupResult(entry: cacheEntry); + return _FlexIntrinsicMeasurementLookupResult( + entry: cacheEntry, + flowChild: flowChild, + ); } void _storeIntrinsicMeasurementCache( @@ -2916,8 +3123,11 @@ class RenderFlexLayout extends RenderLayoutBox { BoxConstraints childConstraints, Size childSize, double intrinsicMainSize, - ) { - final RenderFlowLayout? flowChild = + { + RenderFlowLayout? flowChild, + int? reusableStateSignature, + }) { + flowChild ??= _getCacheableIntrinsicMeasureFlowChild(child, allowAnonymous: true); if (flowChild == null) { return; @@ -2925,17 +3135,39 @@ class RenderFlexLayout extends RenderLayoutBox { final _FlexIntrinsicMeasurementCacheBucket bucket = _childrenIntrinsicMeasureCache[child] ?? _FlexIntrinsicMeasurementCacheBucket(); - bucket.store(_FlexIntrinsicMeasurementCacheEntry( + reusableStateSignature ??= + _computeReusableIntrinsicMeasurementStateSignature( + child, + childConstraints, + flowChild: flowChild, + ); + final _FlexIntrinsicMeasurementCacheEntry entry = + _FlexIntrinsicMeasurementCacheEntry( constraints: childConstraints, size: Size.copy(childSize), intrinsicMainSize: intrinsicMainSize, - reusableStateSignature: _computeReusableIntrinsicMeasurementStateSignature( - child, - childConstraints, - flowChild: flowChild, - ), - )); + reusableStateSignature: reusableStateSignature, + ); + bucket.store(entry); _childrenIntrinsicMeasureCache[child] = bucket; + final int? sharedPlainTextSignature = + _computePlainTextSharedIntrinsicMeasurementSignature( + child, + childConstraints, + flowChild: flowChild, + ); + if (sharedPlainTextSignature != null) { + if (_sharedPlainTextFlexIntrinsicMeasurementCache.length >= 512 && + !_sharedPlainTextFlexIntrinsicMeasurementCache.containsKey( + sharedPlainTextSignature, + )) { + _sharedPlainTextFlexIntrinsicMeasurementCache.remove( + _sharedPlainTextFlexIntrinsicMeasurementCache.keys.first, + ); + } + _sharedPlainTextFlexIntrinsicMeasurementCache[ + sharedPlainTextSignature] = entry; + } } bool _shouldRequirePostMeasureLayout(RenderBox child) { @@ -2970,6 +3202,23 @@ class RenderFlexLayout extends RenderLayoutBox { _childrenRequirePostMeasureLayout[child] = false; } + @pragma('vm:prefer-inline') + bool _canSkipAdjustedFlexChildLayout( + RenderBox child, + RenderBoxModel effectiveChild, + BoxConstraints childConstraints, + ) { + if (child.constraints != childConstraints || + effectiveChild.needsRelayout || + (_childrenRequirePostMeasureLayout[child] == true) || + _subtreeHasPendingIntrinsicMeasureInvalidation(child)) { + return false; + } + + _childrenRequirePostMeasureLayout[child] = false; + return true; + } + _FlexFastPathRejectReason? _getEarlyNoFlexNoStretchNoBaselineRejectReason( RenderBox child, BoxConstraints childConstraints) { if (child is RenderPositionPlaceholder) { @@ -3025,7 +3274,7 @@ class RenderFlexLayout extends RenderLayoutBox { } bool _isMetricsOnlyIntrinsicMeasureChild(RenderBox child) { - final Expando? eligibilityCache = + final Map? eligibilityCache = _metricsOnlyIntrinsicMeasureChildEligibilityCache; final int? cachedValue = eligibilityCache?[child]; if (cachedValue != null) { @@ -3390,12 +3639,6 @@ class RenderFlexLayout extends RenderLayoutBox { final double childMainSize = _getMainSize(child); _childrenIntrinsicMainSizes[child] = childMainSize; - if (runChildren.isNotEmpty) { - runMainAxisExtent += mainAxisGap; - } - runMainAxisExtent += _getMainAxisExtent(child); - runCrossAxisExtent = math.max(runCrossAxisExtent, _getCrossAxisExtent(child)); - final RenderBoxModel? effectiveChild = child is RenderBoxModel ? child : null; final _RunChild runChild = _createRunChildMetadata( child, @@ -3403,6 +3646,12 @@ class RenderFlexLayout extends RenderLayoutBox { effectiveChild: effectiveChild, usedFlexBasis: effectiveChild != null ? _getUsedFlexBasis(child) : null, ); + if (runChildren.isNotEmpty) { + runMainAxisExtent += mainAxisGap; + } + runMainAxisExtent += _getRunChildMainAxisExtent(runChild); + runCrossAxisExtent = + math.max(runCrossAxisExtent, _getRunChildCrossAxisExtent(runChild)); runChildren.add(runChild); if (runChild.flexGrow > 0) { @@ -3441,13 +3690,13 @@ class RenderFlexLayout extends RenderLayoutBox { } void _doPerformLayout() { - if (!kReleaseMode) { + if (_enableFlexProfileSections) { developer.Timeline.startSync('RenderFlex.beforeLayout'); } beforeLayout(); - if (!kReleaseMode) { + if (_enableFlexProfileSections) { developer.Timeline.finishSync(); } @@ -3472,7 +3721,7 @@ class RenderFlexLayout extends RenderLayoutBox { child = childParentData.nextSibling; } - if (!kReleaseMode) { + if (_enableFlexProfileSections) { developer.Timeline.startSync('RenderFlex.layoutPositionedChild'); } @@ -3483,11 +3732,11 @@ class RenderFlexLayout extends RenderLayoutBox { CSSPositionedLayout.layoutPositionedChild(this, child); } - if (!kReleaseMode) { + if (_enableFlexProfileSections) { developer.Timeline.finishSync(); } - if (!kReleaseMode) { + if (_enableFlexProfileSections) { developer.Timeline.startSync('RenderFlex._layoutFlexItems'); } @@ -3497,7 +3746,7 @@ class RenderFlexLayout extends RenderLayoutBox { List orderedChildren = _getSortedFlexItems(flexItemChildren); _layoutFlexItems(orderedChildren); - if (!kReleaseMode) { + if (_enableFlexProfileSections) { developer.Timeline.finishSync(); } @@ -3523,7 +3772,7 @@ class RenderFlexLayout extends RenderLayoutBox { } } - if (!kReleaseMode) { + if (_enableFlexProfileSections) { developer.Timeline.startSync('RenderFlex.adjustPositionChildren'); } @@ -3552,7 +3801,7 @@ class RenderFlexLayout extends RenderLayoutBox { } } - if (!kReleaseMode) { + if (_enableFlexProfileSections) { developer.Timeline.finishSync(); } @@ -3568,8 +3817,12 @@ class RenderFlexLayout extends RenderLayoutBox { // 3. Set flex container size according to children size and its own size styles. // 4. Align children according to alignment properties. void _layoutFlexItems(List children) { - _childrenRequirePostMeasureLayout = - Expando('childrenRequirePostMeasureLayout'); + _childrenRequirePostMeasureLayout.clear(); + _childrenIntrinsicMainSizes.clear(); + if (_adjustedConstraintsCachePassId != renderBoxModelLayoutPassId) { + _adjustedConstraintsCachePassId = renderBoxModelLayoutPassId; + _adjustedConstraintsCache.clear(); + } // If no child exists, stop layout. if (children.isEmpty) { @@ -3619,13 +3872,13 @@ class RenderFlexLayout extends RenderLayoutBox { if (runMetrics == null) { // Layout children to compute metrics of flex lines. - if (!kReleaseMode) { + if (_enableFlexProfileSections) { developer.Timeline.startSync('RenderFlex.layoutFlexItems.computeRunMetrics', arguments: {'renderObject': describeIdentity(this)}); } runMetrics = _computeRunMetrics(children); - if (!kReleaseMode) { + if (_enableFlexProfileSections) { developer.Timeline.finishSync(); } } @@ -3633,7 +3886,7 @@ class RenderFlexLayout extends RenderLayoutBox { // Set flex container size. _setContainerSize(runMetrics); - if (!kReleaseMode) { + if (_enableFlexProfileSections) { developer.Timeline.startSync('RenderFlex.layoutFlexItems.adjustChildrenSize'); } @@ -3643,22 +3896,22 @@ class RenderFlexLayout extends RenderLayoutBox { // _runMetrics maybe update after adjust, set flex containerSize again _setContainerSize(runMetrics); - if (!kReleaseMode) { + if (_enableFlexProfileSections) { developer.Timeline.finishSync(); } - if (!kReleaseMode) { + if (_enableFlexProfileSections) { developer.Timeline.startSync('RenderFlex.layoutFlexItems.setChildrenOffset'); } // Set children offset based on flex alignment properties. _setChildrenOffset(runMetrics); - if (!kReleaseMode) { + if (_enableFlexProfileSections) { developer.Timeline.finishSync(); } - if (!kReleaseMode) { + if (_enableFlexProfileSections) { developer.Timeline.startSync('RenderFlex.layoutFlexItems.setMaxScrollableSize'); } @@ -3668,7 +3921,7 @@ class RenderFlexLayout extends RenderLayoutBox { // Set the baseline values for flex items calculateBaseline(); - if (!kReleaseMode) { + if (_enableFlexProfileSections) { developer.Timeline.finishSync(); } } @@ -3946,10 +4199,10 @@ class RenderFlexLayout extends RenderLayoutBox { // PASS 1+2: Intrinsic layout + compute run metrics in one pass. _metricsOnlyIntrinsicMeasureChildEligibilityCache = - Expando('metricsOnlyIntrinsicMeasureChildEligibilityCache'); + Map.identity(); final bool allowAnonymousMetricsOnlyCache = _canUseAnonymousMetricsOnlyCache(children); - _transientChildSizeOverrides = Expando('transientChildSizeOverrides'); + _transientChildSizeOverrides = Map.identity(); try { for (int childIndex = 0; childIndex < children.length; childIndex++) { final RenderBox child = children[childIndex]; @@ -3981,8 +4234,11 @@ class RenderFlexLayout extends RenderLayoutBox { childSize = cacheEntry.size; intrinsicMain = cacheEntry.intrinsicMainSize; _transientChildSizeOverrides![child] = childSize; - if (_shouldRequirePostMeasureLayout(child) || - isMetricsOnlyMeasureChild) { + final bool requiresPostMeasureLayout = + cacheLookup.reusableStateSignature != null && + (_shouldRequirePostMeasureLayout(child) || + isMetricsOnlyMeasureChild); + if (requiresPostMeasureLayout) { _childrenRequirePostMeasureLayout[child] = true; } } else { @@ -4107,7 +4363,14 @@ class RenderFlexLayout extends RenderLayoutBox { } } - _storeIntrinsicMeasurementCache(child, childConstraints, childSize, intrinsicMain); + _storeIntrinsicMeasurementCache( + child, + childConstraints, + childSize, + intrinsicMain, + flowChild: cacheLookup.flowChild, + reusableStateSignature: cacheLookup.reusableStateSignature, + ); } final RenderLayoutParentData? childParentData = child.parentData as RenderLayoutParentData?; @@ -4115,9 +4378,22 @@ class RenderFlexLayout extends RenderLayoutBox { _childrenIntrinsicMainSizes[child] = intrinsicMain; Size? intrinsicChildSize = _getChildSize(child, shouldUseIntrinsicMainSize: true); + final RenderBoxModel? effectiveChild = + child is RenderBoxModel ? child : null; + final double? usedFlexBasis = + effectiveChild != null ? _getUsedFlexBasis(child) : null; + final _RunChild runChild = _createRunChildMetadata( + child, + intrinsicMain, + effectiveChild: effectiveChild, + usedFlexBasis: usedFlexBasis, + ); - double childMainAxisExtent = _getMainAxisExtent(child, shouldUseIntrinsicMainSize: true); - double childCrossAxisExtent = _getCrossAxisExtent(child); + double childMainAxisExtent = _getRunChildMainAxisExtent( + runChild, + shouldUseIntrinsicMainSize: true, + ); + double childCrossAxisExtent = _getRunChildCrossAxisExtent(runChild); // Include gap spacing in flex line limit check double gapSpacing = runChildren.isNotEmpty ? mainAxisGap : 0; bool isExceedFlexLineLimit = runMainAxisExtent + gapSpacing + childMainAxisExtent > flexLineLimit; @@ -4151,7 +4427,7 @@ class RenderFlexLayout extends RenderLayoutBox { // Vertical align is only valid for inline box. // Baseline alignment in column direction behave the same as flex-start. - AlignSelf alignSelf = _getAlignSelf(child); + AlignSelf alignSelf = runChild.alignSelf; bool isBaselineAlign = alignSelf == AlignSelf.baseline || alignSelf == AlignSelf.lastBaseline || @@ -4200,9 +4476,6 @@ class RenderFlexLayout extends RenderLayoutBox { // We store the base size in runChild.originalMainSize so remaining free // space and shrink/grow weighting use the correct base, and keep the // clamped value in _childrenIntrinsicMainSizes for line metrics. - final RenderBoxModel? effectiveChild = child is RenderBoxModel ? child : null; - final double? usedFlexBasis = effectiveChild != null ? _getUsedFlexBasis(child) : null; - double baseMainSize; if (usedFlexBasis != null) { // Used basis is already border-box (>= padding+border) for non-zero bases. @@ -4221,12 +4494,7 @@ class RenderFlexLayout extends RenderLayoutBox { } // Use clamped intrinsic main size as the hypothetical size for line metrics. - final _RunChild runChild = _createRunChildMetadata( - child, - baseMainSize, - effectiveChild: effectiveChild, - usedFlexBasis: usedFlexBasis, - ); + runChild.originalMainSize = baseMainSize; runChildren.add(runChild); childParentData!.runIndex = runMetrics.length; @@ -4460,15 +4728,17 @@ class RenderFlexLayout extends RenderLayoutBox { // Always enforce min/max constraints on the flex item, regardless of overflow clipping. // Overflow does not disable max-width/height. { - // Apply min/max against the flex item itself (the element). RenderEventListener - // is a RenderBoxModel; do not unwrap to its child. - RenderBoxModel? clampTarget = runChild.effectiveChild; + final RenderBoxModel? clampTarget = runChild.effectiveChild; if (clampTarget != null) { - double minMainAxisSize = _getMinMainAxisSize(clampTarget); - double maxMainAxisSize = _getMaxMainAxisSize(clampTarget); - if (computedSize < minMainAxisSize && (computedSize - minMainAxisSize).abs() >= minFlexPrecision) { + final double minMainAxisSize = + _getRunChildMinMainAxisSize(runChild); + final double maxMainAxisSize = + _getRunChildMaxMainAxisSize(runChild); + if (computedSize < minMainAxisSize && + (computedSize - minMainAxisSize).abs() >= minFlexPrecision) { flexedMainSize = minMainAxisSize; - } else if (computedSize > maxMainAxisSize && (computedSize - maxMainAxisSize).abs() >= minFlexPrecision) { + } else if (computedSize > maxMainAxisSize && + (computedSize - maxMainAxisSize).abs() >= minFlexPrecision) { flexedMainSize = maxMainAxisSize; } } @@ -4674,7 +4944,7 @@ class RenderFlexLayout extends RenderLayoutBox { final RenderBoxModel? effectiveChild = runChild.effectiveChild; if (effectiveChild == null) continue; - final double childOldMainSize = _getMainSize(child); + final double childOldMainSize = _getRunChildMainSize(runChild); final double? desiredPreservedMain = _childrenIntrinsicMainSizes[child]; _FlexAdjustFastPathRelayoutReason? relayoutReason; BoxConstraints? relayoutConstraints; @@ -4817,9 +5087,10 @@ class RenderFlexLayout extends RenderLayoutBox { double crossAxisExtent = 0; for (int i = 0; i < runChildrenList.length; i++) { if (i > 0) mainAxisExtent += mainAxisGap; - final RenderBox child = runChildrenList[i].child; - mainAxisExtent += _getMainAxisExtent(child); - crossAxisExtent = math.max(crossAxisExtent, _getCrossAxisExtent(child)); + final _RunChild runChild = runChildrenList[i]; + mainAxisExtent += _getRunChildMainAxisExtent(runChild); + crossAxisExtent = + math.max(crossAxisExtent, _getRunChildCrossAxisExtent(runChild)); } metrics.mainAxisExtent = mainAxisExtent; metrics.crossAxisExtent = crossAxisExtent; @@ -4844,8 +5115,16 @@ class RenderFlexLayout extends RenderLayoutBox { final double mainAxisGap = _getMainAxisGap(); final bool hasBaselineAlignment = _hasBaselineAlignedChildren(runMetrics); final bool canAttemptFastPath = isHorizontal && !hasBaselineAlignment; - final bool hasStretchedChildren = - canAttemptFastPath ? _hasStretchedChildrenInCrossAxis(runMetrics) : true; + final bool hasStretchedChildren = _hasStretchedChildrenInCrossAxis(runMetrics); + + if (_canSkipAdjustChildrenSize( + runMetrics, + hasBaselineAlignment: hasBaselineAlignment, + hasStretchedChildren: hasStretchedChildren, + )) { + return; + } + final _FlexResolutionInputs inputs = _computeFlexResolutionInputs(); final double? contentBoxLogicalWidth = inputs.contentBoxLogicalWidth; final double? contentBoxLogicalHeight = inputs.contentBoxLogicalHeight; @@ -5023,7 +5302,7 @@ class RenderFlexLayout extends RenderLayoutBox { // Non-RenderBoxModel child: nothing to tighten in phase 1. continue; } - double childOldMainSize = _getMainSize(child); + double childOldMainSize = _getRunChildMainSize(runChild); // Determine used main size from the flexible lengths result, if any. double? childFlexedMainSize; @@ -5085,6 +5364,13 @@ class RenderFlexLayout extends RenderLayoutBox { runChildrenList.length, preserveMainAxisSize: desiredPreservedMain, ); + if (_canSkipAdjustedFlexChildLayout( + child, + effectiveChild, + childConstraints, + )) { + continue; + } _layoutChildForFlex(child, childConstraints); } @@ -5114,9 +5400,9 @@ class RenderFlexLayout extends RenderLayoutBox { if (childStretchedCrossSize == null) continue; // If the current cross size already matches the stretched result, skip. - final double currentCross = _isHorizontalFlexDirection - ? _getChildSize(child)!.height - : _getChildSize(child)!.width; + final double currentCross = + _getRunChildCrossAxisExtent(runChild) - + runChild.crossAxisExtentAdjustment; if ((childStretchedCrossSize - currentCross).abs() < 0.5) continue; // Apply stretch by relayout with tightened cross-axis constraint. @@ -5126,6 +5412,13 @@ class RenderFlexLayout extends RenderLayoutBox { childStretchedCrossSize, runChildrenList.length, ); + if (_canSkipAdjustedFlexChildLayout( + child, + effectiveChild, + childConstraints, + )) { + continue; + } _layoutChildForFlex(child, childConstraints); } @@ -5133,7 +5426,7 @@ class RenderFlexLayout extends RenderLayoutBox { double mainAxisExtent = 0; for (int i = 0; i < runChildrenList.length; i++) { if (i > 0) mainAxisExtent += mainAxisGap; - mainAxisExtent += _getMainAxisExtent(runChildrenList[i].child); + mainAxisExtent += _getRunChildMainAxisExtent(runChildrenList[i]); } metrics.mainAxisExtent = mainAxisExtent; metrics.crossAxisExtent = _recomputeRunCrossExtent(metrics); @@ -5156,12 +5449,7 @@ class RenderFlexLayout extends RenderLayoutBox { final Size childSize = _getChildSize(child)!; double childMainSize = isHorizontal ? childSize.width : childSize.height; double childCrossSize = isHorizontal ? childSize.height : childSize.width; - double childCrossMargin = 0; - if (child is RenderBoxModel) { - childCrossMargin = isHorizontal - ? child.renderStyle.marginTop.computedValue + child.renderStyle.marginBottom.computedValue - : child.renderStyle.marginLeft.computedValue + child.renderStyle.marginRight.computedValue; - } + double childCrossMargin = runChild.crossAxisExtentAdjustment; double childCrossExtent = childCrossSize + childCrossMargin; if (runChild.flexedMainSize != childMainSize && @@ -5179,7 +5467,7 @@ class RenderFlexLayout extends RenderLayoutBox { // Vertical align is only valid for inline box. // Baseline alignment in column direction behave the same as flex-start. - AlignSelf alignSelf = _getAlignSelf(child); + AlignSelf alignSelf = runChild.alignSelf; bool isBaselineAlign = alignSelf == AlignSelf.baseline || alignSelf == AlignSelf.lastBaseline || @@ -5244,6 +5532,51 @@ class RenderFlexLayout extends RenderLayoutBox { // Use stored percentage constraints if available, otherwise use current constraints BoxConstraints oldConstraints = _childrenOldConstraints[child] ?? child.constraints; + final bool canUseAdjustedConstraintsCache = + _adjustedConstraintsCachePassId == renderBoxModelLayoutPassId; + final bool hasOverrideContentLogicalWidth = + child.hasOverrideContentLogicalWidth; + final bool hasOverrideContentLogicalHeight = + child.hasOverrideContentLogicalHeight; + final double? contentBoxLogicalWidth = + child.renderStyle.contentBoxLogicalWidth; + final double? contentBoxLogicalHeight = + child.renderStyle.contentBoxLogicalHeight; + + if (canUseAdjustedConstraintsCache) { + final _AdjustedConstraintsCacheEntry? cachedEntry = + _adjustedConstraintsCache[child]; + if (cachedEntry != null && + cachedEntry.oldConstraints == oldConstraints && + cachedEntry.childFlexedMainSize == childFlexedMainSize && + cachedEntry.childStretchedCrossSize == childStretchedCrossSize && + cachedEntry.lineChildrenCount == lineChildrenCount && + cachedEntry.preserveMainAxisSize == preserveMainAxisSize && + cachedEntry.hasOverrideContentLogicalWidth == + hasOverrideContentLogicalWidth && + cachedEntry.hasOverrideContentLogicalHeight == + hasOverrideContentLogicalHeight && + cachedEntry.contentBoxLogicalWidth == contentBoxLogicalWidth && + cachedEntry.contentBoxLogicalHeight == contentBoxLogicalHeight) { + return cachedEntry.constraints; + } + } + + void cacheAdjustedConstraints(BoxConstraints constraints) { + if (!canUseAdjustedConstraintsCache) return; + _adjustedConstraintsCache[child] = _AdjustedConstraintsCacheEntry( + oldConstraints: oldConstraints, + childFlexedMainSize: childFlexedMainSize, + childStretchedCrossSize: childStretchedCrossSize, + lineChildrenCount: lineChildrenCount, + preserveMainAxisSize: preserveMainAxisSize, + hasOverrideContentLogicalWidth: hasOverrideContentLogicalWidth, + hasOverrideContentLogicalHeight: hasOverrideContentLogicalHeight, + contentBoxLogicalWidth: contentBoxLogicalWidth, + contentBoxLogicalHeight: contentBoxLogicalHeight, + constraints: constraints, + ); + } // Row flex container + pure cross-axis stretch: // Preserve the flex-resolved main size (oldConstraints.min/maxWidth) and @@ -5266,6 +5599,7 @@ class RenderFlexLayout extends RenderLayoutBox { minHeight: h, maxHeight: h, ); + cacheAdjustedConstraints(res); return res; } @@ -5763,6 +6097,7 @@ class RenderFlexLayout extends RenderLayoutBox { maxHeight: adjustedMaxHeight, ); + cacheAdjustedConstraints(childConstraints); return childConstraints; } @@ -5917,18 +6252,7 @@ class RenderFlexLayout extends RenderLayoutBox { final List<_RunChild> runChildren = runMetrics.runChildren; double runMainExtent = 0; for (final _RunChild runChild in runChildren) { - final RenderBox child = runChild.child; - double runChildMainSize = _getMainSize(child); - // Should add main axis margin of child to the main axis auto size of parent. - if (child is RenderBoxModel) { - double childMarginTop = child.renderStyle.marginTop.computedValue; - double childMarginBottom = child.renderStyle.marginBottom.computedValue; - double childMarginLeft = child.renderStyle.marginLeft.computedValue; - double childMarginRight = child.renderStyle.marginRight.computedValue; - runChildMainSize += - _isHorizontalFlexDirection ? childMarginLeft + childMarginRight : childMarginTop + childMarginBottom; - } - runMainExtent += runChildMainSize; + runMainExtent += _getRunChildMainAxisExtent(runChild); } runMainSize.add(runMainExtent); } @@ -6095,15 +6419,7 @@ class RenderFlexLayout extends RenderLayoutBox { maxScrollableCrossSizeInLine = math.max(maxScrollableCrossSizeInLine, childScrollableCross); // Update running main size for subsequent siblings (border-box size + main-axis margins). - double childMainSize = _getMainSize(child); - if (child is RenderBoxModel) { - if (_isHorizontalFlexDirection) { - childMainSize += child.renderStyle.marginLeft.computedValue + child.renderStyle.marginRight.computedValue; - } else { - childMainSize += child.renderStyle.marginTop.computedValue + child.renderStyle.marginBottom.computedValue; - } - } - preSiblingsMainSize += childMainSize; + preSiblingsMainSize += _getRunChildMainAxisExtent(runChild); // Add main-axis gap between items (not after the last item). if (runChild != runChildren.last) { preSiblingsMainSize += _getMainAxisGap(); @@ -6260,6 +6576,14 @@ class RenderFlexLayout extends RenderLayoutBox { if (contentConstraints != null && contentConstraints!.hasBoundedWidth && contentConstraints!.maxWidth.isFinite) { resolvedContainerCross = contentConstraints!.maxWidth; + } else if (constraints.hasBoundedWidth && constraints.maxWidth.isFinite) { + final double borderH = renderStyle.effectiveBorderLeftWidth.computedValue + + renderStyle.effectiveBorderRightWidth.computedValue; + final double paddingH = + renderStyle.paddingLeft.computedValue + + renderStyle.paddingRight.computedValue; + resolvedContainerCross = + math.max(0, constraints.maxWidth - borderH - paddingH); } } // For column-direction containers with width:auto, do not treat the measured @@ -6528,7 +6852,7 @@ class RenderFlexLayout extends RenderLayoutBox { final double childStartMargin = runChild.mainAxisStartMargin; final double childEndMargin = runChild.mainAxisEndMargin; // Border-box main-size of the child (no margins). - final double childMainSizeOnly = _getMainSize(child); + final double childMainSizeOnly = _getRunChildMainSize(runChild); // Position the child along the main axis respecting direction. final bool mainAxisStartAuto = isHorizontal @@ -6619,7 +6943,7 @@ class RenderFlexLayout extends RenderLayoutBox { } } - final double childCrossAxisExtent = _getCrossAxisExtent(child); + final double childCrossAxisExtent = _getRunChildCrossAxisExtent(runChild); childCrossPosition = _getChildCrossAxisOffset( alignment, child, @@ -6784,6 +7108,15 @@ class RenderFlexLayout extends RenderLayoutBox { if (renderStyle.contentBoxLogicalWidth != null) return true; if (contentConstraints != null && contentConstraints!.hasTightWidth) return true; if (constraints.hasTightWidth) return true; + final bool canResolveAutoWidthFromBounds = + renderStyle.writingMode == CSSWritingMode.horizontalTb && + renderStyle.effectiveDisplay != CSSDisplay.inlineFlex && + renderStyle.width.isAuto && + ((contentConstraints?.hasBoundedWidth ?? false) || + constraints.hasBoundedWidth); + if (canResolveAutoWidthFromBounds) { + return true; + } return false; } } @@ -7054,7 +7387,7 @@ class RenderFlexLayout extends RenderLayoutBox { bool _areAllRunChildrenAtMaxCrossExtent(List<_RunChild> runChildren, double runCrossAxisExtent) { const double kEpsilon = 0.0001; for (final _RunChild runChild in runChildren) { - final double childCrossAxisExtent = _getCrossAxisExtent(runChild.child); + final double childCrossAxisExtent = _getRunChildCrossAxisExtent(runChild); if ((childCrossAxisExtent - runCrossAxisExtent).abs() > kEpsilon) { return false; } @@ -7062,6 +7395,67 @@ class RenderFlexLayout extends RenderLayoutBox { return true; } + bool _canSkipAdjustChildrenSize( + List<_RunMetrics> runMetrics, { + required bool hasBaselineAlignment, + required bool hasStretchedChildren, + }) { + if (runMetrics.isEmpty || hasBaselineAlignment) { + return false; + } + + final bool isHorizontal = _isHorizontalFlexDirection; + final double availCross = contentConstraints?.maxWidth ?? double.infinity; + + for (final _RunMetrics metrics in runMetrics) { + if (metrics.totalFlexGrow > 0 || metrics.totalFlexShrink > 0) { + return false; + } + + if (hasStretchedChildren && + !_areAllRunChildrenAtMaxCrossExtent( + metrics.runChildren, + metrics.crossAxisExtent, + )) { + return false; + } + + for (final _RunChild runChild in metrics.runChildren) { + final RenderBox child = runChild.child; + final RenderBoxModel? effectiveChild = runChild.effectiveChild; + if (effectiveChild == null) { + return false; + } + + if (effectiveChild.needsRelayout || + _childrenRequirePostMeasureLayout[child] == true || + _subtreeHasPendingIntrinsicMeasureInvalidation(child)) { + return false; + } + + final double? desiredPreservedMain = _childrenIntrinsicMainSizes[child]; + if (desiredPreservedMain != null && + (desiredPreservedMain - _getRunChildMainSize(runChild)).abs() >= + 0.5) { + return false; + } + + if (!isHorizontal) { + final bool childCrossAuto = effectiveChild.renderStyle.width.isAuto; + if (childCrossAuto && availCross.isFinite) { + final double measuredBorderWidth = + _getChildSize(effectiveChild)!.width; + if (measuredBorderWidth > availCross + 0.5) { + return false; + } + } + } + } + } + + return true; + } + // Compute distance to baseline of flex layout. @override double? computeDistanceToActualBaseline(TextBaseline baseline) { diff --git a/webf/lib/src/rendering/flow.dart b/webf/lib/src/rendering/flow.dart index faa57c4dff..c3d3c33d24 100644 --- a/webf/lib/src/rendering/flow.dart +++ b/webf/lib/src/rendering/flow.dart @@ -10,7 +10,6 @@ import 'dart:math' as math; import 'dart:ui' as ui show Paragraph, ParagraphBuilder, ParagraphConstraints, ParagraphStyle, TextStyle, TextHeightBehavior, LineMetrics, TextLeadingDistribution, Rect; -import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:webf/css.dart'; @@ -275,7 +274,6 @@ class RenderFlowLayout extends RenderLayoutBox { // https://www.w3.org/TR/css-inline-3/#line-boxes // Fow example Hello
world.
will have two line boxes final List _lineMetrics = []; - @override void dispose() { super.dispose(); @@ -1142,7 +1140,8 @@ class RenderFlowLayout extends RenderLayoutBox { // and alignment properties. void _doRegularFlowLayout(List children) { _lineMetrics.clear(); - children.forEachIndexed((index, child) { + for (int index = 0; index < children.length; index++) { + final RenderBox child = children[index]; BoxConstraints childConstraints; if (child is RenderBoxModel) { childConstraints = child.getConstraints(); @@ -1199,7 +1198,7 @@ class RenderFlowLayout extends RenderLayoutBox { } _lineMetrics.add(RunMetrics(childMainAxisExtent, childCrossAxisExtent, [child], baseline: childBaseline)); - }); + } } double _getRunsMaxMainSize(List runMetrics,) { @@ -1326,9 +1325,10 @@ class RenderFlowLayout extends RenderLayoutBox { // Record the main size of all lines. void _recordRunsMainSize(RunMetrics runMetrics, List runMainSize) { - List runChildren = runMetrics.runChildren; double runMainExtent = 0; - void iterateRunChildren(RenderBox runChild) { + final List runChildren = runMetrics.runChildren; + for (int i = 0; i < runChildren.length; i++) { + final RenderBox runChild = runChildren[i]; double runChildMainSize = 0.0; // For automatic minimum size, use each child's min-content contribution // in border-box, plus horizontal margins, instead of the child's used size. @@ -1345,8 +1345,6 @@ class RenderFlowLayout extends RenderLayoutBox { } runMainExtent += runChildMainSize; } - - runChildren.forEach(iterateRunChildren); runMainSize.add(runMainExtent); } @@ -1828,7 +1826,9 @@ class RenderFlowLayout extends RenderLayoutBox { runChildrenList.add(child); } - runChildren.forEach(iterateRunChildren); + for (int i = 0; i < runChildren.length; i++) { + iterateRunChildren(runChildren[i]); + } if (DebugFlags.debugLogScrollableEnabled) { for (int i = 0; i < runChildren.length; i++) { diff --git a/webf/lib/src/rendering/inline_formatting_context.dart b/webf/lib/src/rendering/inline_formatting_context.dart index c02608aabd..474d588113 100644 --- a/webf/lib/src/rendering/inline_formatting_context.dart +++ b/webf/lib/src/rendering/inline_formatting_context.dart @@ -71,6 +71,10 @@ const bool _enableInlineProfileSections = /// Manages the inline formatting context for a block container. /// Based on Blink's InlineNode. class InlineFormattingContext { + static int _ancestorHorizontalScrollCachePassId = -1; + static final Map _ancestorHorizontalScrollCache = + Map.identity(); + InlineFormattingContext({ required this.container, }); @@ -78,12 +82,23 @@ class InlineFormattingContext { /// The block container that establishes this inline formatting context. final RenderLayoutBox container; + @pragma('vm:prefer-inline') T _profileSection(String label, T Function() action, {Map? arguments}) { if (kReleaseMode || !_enableInlineProfileSections) { return action(); } + return _profileSectionEnabled( + label, + action, + arguments: arguments, + ); + } + + @pragma('vm:never-inline') + T _profileSectionEnabled(String label, T Function() action, + {Map? arguments}) { developer.Timeline.startSync( label, arguments: _sanitizeTimelineArguments(arguments), @@ -229,6 +244,9 @@ class InlineFormattingContext { final Map _cachedRectLineIndices = {}; List? _cachedTextRunParagraphsForReuse; bool? _cachedAncestorHasHorizontalScroll; + int _resolvedLayoutStyleSignaturePassId = -1; + final Map _cachedResolvedLayoutStyleSignatures = + Map.identity(); static const int _resolvedLayoutPassCacheLimit = 8; final Map _resolvedLayoutPassCache = {}; @@ -497,6 +515,8 @@ class InlineFormattingContext { _resetParagraphGeometryCaches(); _cachedTextRunParagraphsForReuse = null; _cachedAncestorHasHorizontalScroll = null; + _resolvedLayoutStyleSignaturePassId = -1; + _cachedResolvedLayoutStyleSignatures.clear(); } @pragma('vm:prefer-inline') @@ -523,6 +543,21 @@ class InlineFormattingContext { } int _resolvedLayoutStyleSignature(CSSRenderStyle style) { + final bool useLayoutPassCache = renderBoxModelInLayoutStack.isNotEmpty; + if (useLayoutPassCache) { + final int currentLayoutPassId = renderBoxModelLayoutPassId; + if (_resolvedLayoutStyleSignaturePassId != currentLayoutPassId) { + _resolvedLayoutStyleSignaturePassId = currentLayoutPassId; + _cachedResolvedLayoutStyleSignatures.clear(); + } else { + final int? cachedSignature = + _cachedResolvedLayoutStyleSignatures[style]; + if (cachedSignature != null) { + return cachedSignature; + } + } + } + int hash = 0; hash = _hashCombineInt(hash, style.display.hashCode); hash = _hashCombineInt(hash, style.direction.hashCode); @@ -556,7 +591,11 @@ class InlineFormattingContext { hash = _hashCombineInt(hash, _quantizeDouble(style.effectiveBorderRightWidth.computedValue)); hash = _hashCombineInt(hash, _quantizeDouble(style.effectiveBorderTopWidth.computedValue)); hash = _hashCombineInt(hash, _quantizeDouble(style.effectiveBorderBottomWidth.computedValue)); - return _hashFinishInt(hash); + final int signature = _hashFinishInt(hash); + if (useLayoutPassCache) { + _cachedResolvedLayoutStyleSignatures[style] = signature; + } + return signature; } int _resolvedLayoutItemSignature(InlineItem item) { @@ -752,8 +791,37 @@ class InlineFormattingContext { if (cached != null) { return cached; } - RenderObject? p = container.parent; + + final bool useLayoutPassCache = renderBoxModelInLayoutStack.isNotEmpty; + final RenderObject? start = container.parent; + if (useLayoutPassCache) { + final int currentLayoutPassId = renderBoxModelLayoutPassId; + if (_ancestorHorizontalScrollCachePassId != currentLayoutPassId) { + _ancestorHorizontalScrollCachePassId = currentLayoutPassId; + _ancestorHorizontalScrollCache.clear(); + } else if (start != null) { + final bool? sharedCached = _ancestorHorizontalScrollCache[start]; + if (sharedCached != null) { + _cachedAncestorHasHorizontalScroll = sharedCached; + return sharedCached; + } + } + } + + final List visited = + useLayoutPassCache ? [] : const []; + RenderObject? p = start; + bool hasHorizontalScroll = false; while (p != null) { + if (useLayoutPassCache) { + final bool? sharedCached = _ancestorHorizontalScrollCache[p]; + if (sharedCached != null) { + hasHorizontalScroll = sharedCached; + break; + } + visited.add(p); + } + if (p is RenderBoxModel) { // Ignore the document root and body (page scroller); they should not // trigger wide shaping for general layout like flex items. @@ -764,16 +832,22 @@ class InlineFormattingContext { } final o = p.renderStyle.effectiveOverflowX; if (o == CSSOverflowType.scroll || o == CSSOverflowType.auto) { - _cachedAncestorHasHorizontalScroll = true; - return true; + hasHorizontalScroll = true; + break; } } // Stop at widget boundary to avoid leaking outside this subtree. if (p is RenderWidget) break; p = p.parent; } - _cachedAncestorHasHorizontalScroll = false; - return false; + + if (useLayoutPassCache) { + for (int i = 0; i < visited.length; i++) { + _ancestorHorizontalScrollCache[visited[i]] = hasHorizontalScroll; + } + } + _cachedAncestorHasHorizontalScroll = hasHorizontalScroll; + return hasHorizontalScroll; } bool _whiteSpaceEligibleForNoWordBreak(WhiteSpace ws) { @@ -1473,6 +1547,49 @@ class InlineFormattingContext { return _resolvedLayoutPassSignature(constraints); } + bool get isPlainTextOnlyForSharedReuse { + prepareLayout(); + for (final InlineItem item in _items) { + switch (item.type) { + case InlineItemType.text: + case InlineItemType.control: + case InlineItemType.bidiControl: + case InlineItemType.lineBreakOpportunity: + if (item.renderBox != null) { + return false; + } + break; + default: + return false; + } + } + return true; + } + + int plainTextSharedReuseSignature() { + prepareLayout(); + int hash = 0; + hash = _hashCombineInt(hash, _items.length); + hash = _hashCombineInt(hash, _textContent.hashCode); + hash = _hashCombineInt( + hash, + _resolvedLayoutStyleSignature((container as RenderBoxModel).renderStyle), + ); + for (final InlineItem item in _items) { + hash = _hashCombineInt(hash, item.type.hashCode); + hash = _hashCombineInt(hash, item.startOffset); + hash = _hashCombineInt(hash, item.endOffset); + if (item.direction != null) { + hash = _hashCombineInt(hash, item.direction.hashCode); + } + final CSSRenderStyle? style = item.style; + if (style != null) { + hash = _hashCombineInt(hash, _resolvedLayoutStyleSignature(style)); + } + } + return _hashFinishInt(hash); + } + // Expose paragraph intrinsic widths when available. // CSS min-content width depends on white-space: // - For normal/pre-wrap: roughly the longest unbreakable segment (“word”). @@ -1842,7 +1959,7 @@ class InlineFormattingContext { /// Perform layout with given constraints. Size layout(BoxConstraints constraints) { - if (!kReleaseMode) { + if (!kReleaseMode && _enableInlineProfileSections) { developer.Timeline.startSync('InlineFormattingContext.layout'); } try { @@ -2132,7 +2249,7 @@ class InlineFormattingContext { _applyAtomicInlineParentDataOffsets(); return Size(width, height); } finally { - if (!kReleaseMode) { + if (!kReleaseMode && _enableInlineProfileSections) { developer.Timeline.finishSync(); } } diff --git a/webf/lib/src/rendering/layout_box.dart b/webf/lib/src/rendering/layout_box.dart index d94a3592be..1cc0d108a0 100644 --- a/webf/lib/src/rendering/layout_box.dart +++ b/webf/lib/src/rendering/layout_box.dart @@ -746,6 +746,7 @@ abstract class RenderLayoutBox extends RenderBoxModel required double contentWidth, required double contentHeight, }) { + final EdgeInsets margin = renderStyle.margin; double finalContentWidth = contentWidth; double finalContentHeight = contentHeight; @@ -754,8 +755,8 @@ abstract class RenderLayoutBox extends RenderBoxModel double? specifiedContentHeight = renderStyle.contentBoxLogicalHeight; // Margin negative will set element which is static && not set width, size bigger - double? marginLeft = renderStyle.marginLeft.computedValue; - double? marginRight = renderStyle.marginRight.computedValue; + double? marginLeft = margin.left; + double? marginRight = margin.right; double? marginAddSizeLeft = 0; double? marginAddSizeRight = 0; if (isNegativeMarginChangeHSize) { @@ -882,35 +883,19 @@ abstract class RenderLayoutBox extends RenderBoxModel } Size finalContentSize = Size(finalContentWidth, finalContentHeight); - - try { - // Keep for future diagnostic hooks (no-op by default). - final tag = renderStyle.target.tagName.toLowerCase(); - final paddL = renderStyle.paddingLeft.computedValue; - final paddR = renderStyle.paddingRight.computedValue; - final bordL = renderStyle.effectiveBorderLeftWidth.computedValue; - final bordR = renderStyle.effectiveBorderRightWidth.computedValue; - final _ = '[BoxSize] <$tag> getContentSize out=' - '${finalContentSize.width.toStringAsFixed(2)}×${finalContentSize.height.toStringAsFixed(2)} ' - 'padH=${(paddL + paddR).toStringAsFixed(2)} borderH=${(bordL + bordR).toStringAsFixed(2)}'; - } catch (_) {} return finalContentSize; } double _getContentWidth(double width) { - return width - - (renderStyle.borderLeftWidth?.computedValue ?? 0) - - (renderStyle.borderRightWidth?.computedValue ?? 0) - - renderStyle.paddingLeft.computedValue - - renderStyle.paddingRight.computedValue; + final EdgeInsets padding = renderStyle.padding; + final EdgeInsets border = renderStyle.border; + return width - border.horizontal - padding.horizontal; } double _getContentHeight(double height) { - return height - - (renderStyle.borderTopWidth?.computedValue ?? 0) - - (renderStyle.borderBottomWidth?.computedValue ?? 0) - - renderStyle.paddingTop.computedValue - - renderStyle.paddingBottom.computedValue; + final EdgeInsets padding = renderStyle.padding; + final EdgeInsets border = renderStyle.border; + return height - border.vertical - padding.vertical; } @override From ad99480965ac9b8945fabc50980ac76cb190a6b0 Mon Sep 17 00:00:00 2001 From: andycall Date: Sat, 28 Mar 2026 01:33:02 -0700 Subject: [PATCH 10/13] perf(webf): narrow flex cache reuse under wrapped layouts --- webf/lib/src/rendering/box_wrapper.dart | 50 +++++- webf/lib/src/rendering/event_listener.dart | 43 ++++- webf/lib/src/rendering/flex.dart | 180 ++++++++++++++++----- webf/lib/src/rendering/flow.dart | 38 ++++- 4 files changed, 268 insertions(+), 43 deletions(-) diff --git a/webf/lib/src/rendering/box_wrapper.dart b/webf/lib/src/rendering/box_wrapper.dart index 7a3e4c0093..6dd5fdf7bf 100644 --- a/webf/lib/src/rendering/box_wrapper.dart +++ b/webf/lib/src/rendering/box_wrapper.dart @@ -13,12 +13,47 @@ import 'package:webf/css.dart'; import 'package:webf/rendering.dart'; import 'package:webf/dom.dart' as dom; +bool _canReuseWrappedChildLayout(RenderBox? child, BoxConstraints constraints) { + if (child == null || !child.hasSize || child.constraints != constraints) { + return false; + } + if (child.debugNeedsLayout) { + return false; + } + if (child is RenderTextBox && child.hasPendingTextLayoutUpdate) { + return false; + } + if (child is RenderBoxModel && + (child.needsRelayout || + child.hasPendingSubtreeIntrinsicMeasurementInvalidation)) { + return false; + } + return true; +} + class RenderLayoutBoxWrapper extends RenderBoxModel with RenderObjectWithChildMixin, RenderProxyBoxMixin { RenderLayoutBoxWrapper({ required super.renderStyle, }); + BoxConstraints? _lastWrapperConstraints; + BoxConstraints? _lastResolvedChildConstraints; + + bool _canReuseOwnLayout(RenderBox? child) { + final BoxConstraints? lastWrapperConstraints = _lastWrapperConstraints; + final BoxConstraints? lastChildConstraints = _lastResolvedChildConstraints; + if (!hasSize || + child == null || + lastWrapperConstraints == null || + lastChildConstraints == null || + constraints != lastWrapperConstraints || + needsRelayout) { + return false; + } + return _canReuseWrappedChildLayout(child, lastChildConstraints); + } + @override void describeSemanticsConfiguration(SemanticsConfiguration config) { // Intentionally do NOT call super (RenderBoxModel) here. @@ -86,6 +121,15 @@ class RenderLayoutBoxWrapper extends RenderBoxModel if (c == null) { size = constraints.constrain(Size.zero); initOverflowLayout(Rect.fromLTRB(0, 0, size.width, size.height), Rect.fromLTRB(0, 0, size.width, size.height)); + _lastWrapperConstraints = constraints; + _lastResolvedChildConstraints = null; + return; + } + + if (_canReuseOwnLayout(c)) { + if (c is RenderBoxModel) { + scrollableSize = c.scrollableSize; + } return; } @@ -132,8 +176,12 @@ class RenderLayoutBoxWrapper extends RenderBoxModel } childConstraints = intersect(childConstraints, constraints); + _lastWrapperConstraints = constraints; + _lastResolvedChildConstraints = childConstraints; - c.layout(childConstraints, parentUsesSize: true); + if (!_canReuseWrappedChildLayout(c, childConstraints)) { + c.layout(childConstraints, parentUsesSize: true); + } if (c is RenderBoxModel) { // For list-like widget containers (ListView children), sibling margin diff --git a/webf/lib/src/rendering/event_listener.dart b/webf/lib/src/rendering/event_listener.dart index 96038ac3d8..07d9ce30b6 100644 --- a/webf/lib/src/rendering/event_listener.dart +++ b/webf/lib/src/rendering/event_listener.dart @@ -14,6 +14,24 @@ import 'package:webf/launcher.dart'; import 'package:webf/gesture.dart'; import 'package:webf/rendering.dart' hide RenderBoxContainerDefaultsMixin; +bool _canReuseProxyChildLayout(RenderBox? child, BoxConstraints constraints) { + if (child == null || !child.hasSize || child.constraints != constraints) { + return false; + } + if (child.debugNeedsLayout) { + return false; + } + if (child is RenderTextBox && child.hasPendingTextLayoutUpdate) { + return false; + } + if (child is RenderBoxModel && + (child.needsRelayout || + child.hasPendingSubtreeIntrinsicMeasurementInvalidation)) { + return false; + } + return true; +} + class RenderPortalsParentData extends RenderLayoutParentData {} class RenderEventListener extends RenderBoxModel @@ -49,6 +67,8 @@ class RenderEventListener extends RenderBoxModel GestureDispatcher? get gestureDispatcher => _gestureDispatcher; + BoxConstraints? _lastLayoutConstraints; + @override void applyPaintTransform(RenderObject child, Matrix4 transform) { final BoxParentData childParentData = child.parentData as BoxParentData; @@ -170,16 +190,31 @@ class RenderEventListener extends RenderBoxModel @override void performLayout() { updateIntrinsicMeasurementInvalidationForCurrentLayoutPass(); - size = (child?..layout(constraints, parentUsesSize: true))?.size ?? - computeSizeForNoChild(constraints); + final RenderBox? currentChild = child; + if (hasSize && + !needsRelayout && + _lastLayoutConstraints == constraints && + _canReuseProxyChildLayout(currentChild, constraints)) { + if (currentChild is RenderBoxModel) { + scrollableSize = currentChild.scrollableSize; + } + return; + } + if (_canReuseProxyChildLayout(currentChild, constraints)) { + size = currentChild!.size; + } else { + size = (currentChild?..layout(constraints, parentUsesSize: true))?.size ?? + computeSizeForNoChild(constraints); + } + _lastLayoutConstraints = constraints; calculateBaseline(); initOverflowLayout(Rect.fromLTRB(0, 0, size.width, size.height), Rect.fromLTRB(0, 0, size.width, size.height)); // Set the size of scrollable overflow area for Portal. - if (child is RenderBoxModel) { - scrollableSize = (child as RenderBoxModel).scrollableSize; + if (currentChild is RenderBoxModel) { + scrollableSize = currentChild.scrollableSize; } } diff --git a/webf/lib/src/rendering/flex.dart b/webf/lib/src/rendering/flex.dart index a5ca2062ba..f4d7a9f2ea 100644 --- a/webf/lib/src/rendering/flex.dart +++ b/webf/lib/src/rendering/flex.dart @@ -660,6 +660,7 @@ class _FlexIntrinsicMeasurementLookupResult { this.missDetails, this.flowChild, this.reusableStateSignature, + this.sharedPlainTextSignature, }); final _FlexIntrinsicMeasurementCacheEntry? entry; @@ -667,6 +668,7 @@ class _FlexIntrinsicMeasurementLookupResult { final Map? missDetails; final RenderFlowLayout? flowChild; final int? reusableStateSignature; + final int? sharedPlainTextSignature; } // Position and size info of each run (flex line) in flex layout. @@ -1237,6 +1239,7 @@ class RenderFlexLayout extends RenderLayoutBox { Map.identity(); Map? _transientChildSizeOverrides; Map? _metricsOnlyIntrinsicMeasureChildEligibilityCache; + Map? _cacheableIntrinsicMeasureFlowChildCache; int _reusableIntrinsicStyleSignaturePassId = -1; final Map _cachedReusableIntrinsicStyleSignatures = Map.identity(); @@ -1258,6 +1261,7 @@ class RenderFlexLayout extends RenderLayoutBox { _adjustedConstraintsCache.clear(); _transientChildSizeOverrides = null; _metricsOnlyIntrinsicMeasureChildEligibilityCache = null; + _cacheableIntrinsicMeasureFlowChildCache = null; _reusableIntrinsicStyleSignaturePassId = -1; _cachedReusableIntrinsicStyleSignatures.clear(); } @@ -2543,25 +2547,43 @@ class RenderFlexLayout extends RenderLayoutBox { RenderBox child, { bool allowAnonymous = false, }) { - if (child is RenderFlowLayout) { - if (child.renderStyle.isSelfAnonymousFlowLayout()) { - if (!allowAnonymous || !_canReuseAnonymousFlowMeasurement(child)) { - return null; + final Map? flowChildCache = + allowAnonymous ? _cacheableIntrinsicMeasureFlowChildCache : null; + if (flowChildCache != null && flowChildCache.containsKey(child)) { + return flowChildCache[child]; + } + + RenderFlowLayout? resolvedFlowChild; + RenderBox? current = child; + int depth = 0; + while (current != null && depth < 3) { + if (current is RenderFlowLayout) { + if (current.renderStyle.isSelfAnonymousFlowLayout()) { + if (!allowAnonymous || !_canReuseAnonymousFlowMeasurement(current)) { + resolvedFlowChild = null; + } else { + resolvedFlowChild = current; + } + } else { + resolvedFlowChild = current; } + break; } - return child; - } - if (child is RenderEventListener) { - final RenderBox? wrapped = child.child; - if (wrapped is RenderFlowLayout) { - if (wrapped.renderStyle.isSelfAnonymousFlowLayout() && - (!allowAnonymous || !_canReuseAnonymousFlowMeasurement(wrapped))) { - return null; - } - return wrapped; + + if (current is RenderEventListener || + current is RenderLayoutBoxWrapper) { + current = (current as dynamic).child as RenderBox?; + depth++; + continue; } + + break; } - return null; + + if (flowChildCache != null) { + flowChildCache[child] = resolvedFlowChild; + } + return resolvedFlowChild; } bool _canReuseAnonymousFlowMeasurement(RenderFlowLayout flowChild) { @@ -2644,7 +2666,8 @@ class RenderFlexLayout extends RenderLayoutBox { return false; } if (metricsOnlyChildCount != children.length && - flexingMetricsOnlyChildCount > 0) { + flexingMetricsOnlyChildCount > 0 && + !_canReuseMixedMetricsOnlyChildren(children)) { _recordAnonymousMetricsReject( _FlexAnonymousMetricsRejectReason.mixedMetricsOnlyChildren, details: { @@ -2663,6 +2686,24 @@ class RenderFlexLayout extends RenderLayoutBox { return true; } + bool _canReuseMixedMetricsOnlyChildren(List children) { + if (_isHorizontalFlexDirection) { + return false; + } + + bool sawCandidate = false; + for (final RenderBox child in children) { + if (!_isMetricsOnlyIntrinsicMeasureChild(child)) { + continue; + } + sawCandidate = true; + if (_getFlexGrow(child) > 0) { + return false; + } + } + return sawCandidate; + } + bool _hasBaselineAlignmentForChild(RenderBox child) { if (renderStyle.alignItems == AlignItems.baseline || renderStyle.alignItems == AlignItems.lastBaseline) { @@ -2949,6 +2990,10 @@ class RenderFlexLayout extends RenderLayoutBox { BoxConstraints childConstraints, { RenderFlowLayout? flowChild, }) { + if (child is! RenderFlowLayout && child is! RenderEventListener) { + return null; + } + final RenderFlowLayout? effectiveFlowChild = flowChild ?? _getCacheableIntrinsicMeasureFlowChild( @@ -2998,14 +3043,14 @@ class RenderFlexLayout extends RenderLayoutBox { if (!allowAnonymous) { return const _FlexIntrinsicMeasurementLookupResult(); } - if (_getMainAxisGap() > 0) { + if (_hasWrappingFlexAncestor()) { return const _FlexIntrinsicMeasurementLookupResult(); } final RenderFlowLayout? flowChild = _getCacheableIntrinsicMeasureFlowChild( child, allowAnonymous: allowAnonymous, ); - if (!_isHorizontalFlexDirection || renderStyle.flexWrap != FlexWrap.nowrap) { + if (renderStyle.flexWrap != FlexWrap.nowrap) { return const _FlexIntrinsicMeasurementLookupResult(); } if (_hasBaselineAlignmentForChild(child)) { @@ -3014,17 +3059,25 @@ class RenderFlexLayout extends RenderLayoutBox { if (flowChild == null) { return const _FlexIntrinsicMeasurementLookupResult(); } - final int? sharedPlainTextSignature = - _computePlainTextSharedIntrinsicMeasurementSignature( - child, - childConstraints, - flowChild: flowChild, - ); + int? sharedPlainTextSignature; + bool didComputeSharedPlainTextSignature = false; + int? getSharedPlainTextSignature() { + if (!didComputeSharedPlainTextSignature) { + didComputeSharedPlainTextSignature = true; + sharedPlainTextSignature = + _computePlainTextSharedIntrinsicMeasurementSignature( + child, + childConstraints, + flowChild: flowChild, + ); + } + return sharedPlainTextSignature; + } final _FlexIntrinsicMeasurementCacheBucket? cacheBucket = _childrenIntrinsicMeasureCache[child]; if (cacheBucket == null || cacheBucket.entries.isEmpty) { final _FlexIntrinsicMeasurementCacheEntry? sharedEntry = - sharedPlainTextSignature == null + getSharedPlainTextSignature() == null ? null : _sharedPlainTextFlexIntrinsicMeasurementCache[ sharedPlainTextSignature @@ -3038,18 +3091,20 @@ class RenderFlexLayout extends RenderLayoutBox { return _FlexIntrinsicMeasurementLookupResult( entry: sharedEntry, flowChild: flowChild, + sharedPlainTextSignature: sharedPlainTextSignature, ); } return _FlexIntrinsicMeasurementLookupResult( flowChild: flowChild, missReason: _FlexAnonymousMetricsMissReason.missingCacheEntry, + sharedPlainTextSignature: sharedPlainTextSignature, ); } final _FlexIntrinsicMeasurementCacheEntry? cacheEntry = cacheBucket.lookupLatest(childConstraints); if (cacheEntry == null) { final _FlexIntrinsicMeasurementCacheEntry? sharedEntry = - sharedPlainTextSignature == null + getSharedPlainTextSignature() == null ? null : _sharedPlainTextFlexIntrinsicMeasurementCache[ sharedPlainTextSignature @@ -3059,11 +3114,13 @@ class RenderFlexLayout extends RenderLayoutBox { return _FlexIntrinsicMeasurementLookupResult( entry: sharedEntry, flowChild: flowChild, + sharedPlainTextSignature: sharedPlainTextSignature, ); } return _FlexIntrinsicMeasurementLookupResult( flowChild: flowChild, missReason: _FlexAnonymousMetricsMissReason.constraintsMismatch, + sharedPlainTextSignature: sharedPlainTextSignature, ); } final bool flowNeedsRelayout = flowChild.needsRelayout; @@ -3085,6 +3142,7 @@ class RenderFlexLayout extends RenderLayoutBox { entry: reusableEntry, flowChild: flowChild, reusableStateSignature: reusableStateSignature, + sharedPlainTextSignature: sharedPlainTextSignature, ); } if (flowNeedsRelayout) { @@ -3092,6 +3150,7 @@ class RenderFlexLayout extends RenderLayoutBox { flowChild: flowChild, reusableStateSignature: reusableStateSignature, missReason: _FlexAnonymousMetricsMissReason.flowNeedsRelayout, + sharedPlainTextSignature: sharedPlainTextSignature, ); } if (childNeedsRelayout) { @@ -3099,6 +3158,7 @@ class RenderFlexLayout extends RenderLayoutBox { flowChild: flowChild, reusableStateSignature: reusableStateSignature, missReason: _FlexAnonymousMetricsMissReason.childNeedsRelayout, + sharedPlainTextSignature: sharedPlainTextSignature, ); } return _FlexIntrinsicMeasurementLookupResult( @@ -3110,11 +3170,13 @@ class RenderFlexLayout extends RenderLayoutBox { missDetails: _FlexAnonymousMetricsProfiler.enabled ? _describeFirstPendingIntrinsicMeasureInvalidation(child) : null, + sharedPlainTextSignature: sharedPlainTextSignature, ); } return _FlexIntrinsicMeasurementLookupResult( entry: cacheEntry, flowChild: flowChild, + sharedPlainTextSignature: sharedPlainTextSignature, ); } @@ -3126,7 +3188,11 @@ class RenderFlexLayout extends RenderLayoutBox { { RenderFlowLayout? flowChild, int? reusableStateSignature, + int? sharedPlainTextSignature, }) { + if (_hasWrappingFlexAncestor()) { + return; + } flowChild ??= _getCacheableIntrinsicMeasureFlowChild(child, allowAnonymous: true); if (flowChild == null) { @@ -3150,7 +3216,7 @@ class RenderFlexLayout extends RenderLayoutBox { ); bucket.store(entry); _childrenIntrinsicMeasureCache[child] = bucket; - final int? sharedPlainTextSignature = + sharedPlainTextSignature ??= _computePlainTextSharedIntrinsicMeasurementSignature( child, childConstraints, @@ -3186,7 +3252,7 @@ class RenderFlexLayout extends RenderLayoutBox { // needs the wrapper's cached boxSize, not RenderBox.size dependency wiring. // RenderEventListener falls back to legacy bubbling only when its parent // did not establish a size dependency. - return child is RenderEventListener; + return child is RenderEventListener || child is RenderLayoutBoxWrapper; } void _layoutChildForFlex(RenderBox child, BoxConstraints childConstraints) { @@ -3202,6 +3268,28 @@ class RenderFlexLayout extends RenderLayoutBox { _childrenRequirePostMeasureLayout[child] = false; } + @pragma('vm:prefer-inline') + bool _canReuseCurrentFlexMeasurement( + RenderBox child, + BoxConstraints childConstraints, { + RenderBoxModel? effectiveChild, + }) { + if (!child.hasSize || child.constraints != childConstraints) { + return false; + } + if (child.debugNeedsLayout) { + return false; + } + if (effectiveChild != null && effectiveChild.needsRelayout) { + return false; + } + if ((_childrenRequirePostMeasureLayout[child] == true) || + _subtreeHasPendingIntrinsicMeasureInvalidation(child)) { + return false; + } + return true; + } + @pragma('vm:prefer-inline') bool _canSkipAdjustedFlexChildLayout( RenderBox child, @@ -3298,7 +3386,10 @@ class RenderFlexLayout extends RenderLayoutBox { return true; } if (flowChild.renderStyle.target is SpanElement) { - return false; + final InlineFormattingContext? ifc = flowChild.inlineFormattingContext; + return ifc != null && + ifc.isPlainTextOnlyForSharedReuse && + flowChild.renderStyle.whiteSpace == WhiteSpace.nowrap; } return _flowSubtreeContainsReusableTextHeavyContent(flowChild); } @@ -3630,7 +3721,15 @@ class RenderFlexLayout extends RenderLayoutBox { return null; } - _layoutChildForFlex(child, childConstraints); + final RenderBoxModel? effectiveChild = + child is RenderBoxModel ? child : null; + if (!_canReuseCurrentFlexMeasurement( + child, + childConstraints, + effectiveChild: effectiveChild, + )) { + _layoutChildForFlex(child, childConstraints); + } _cacheOriginalConstraintsIfNeeded(child, childConstraints); final RenderLayoutParentData? childParentData = child.parentData as RenderLayoutParentData?; @@ -3639,7 +3738,6 @@ class RenderFlexLayout extends RenderLayoutBox { final double childMainSize = _getMainSize(child); _childrenIntrinsicMainSizes[child] = childMainSize; - final RenderBoxModel? effectiveChild = child is RenderBoxModel ? child : null; final _RunChild runChild = _createRunChildMetadata( child, childMainSize, @@ -4200,6 +4298,8 @@ class RenderFlexLayout extends RenderLayoutBox { // PASS 1+2: Intrinsic layout + compute run metrics in one pass. _metricsOnlyIntrinsicMeasureChildEligibilityCache = Map.identity(); + _cacheableIntrinsicMeasureFlowChildCache = + Map.identity(); final bool allowAnonymousMetricsOnlyCache = _canUseAnonymousMetricsOnlyCache(children); _transientChildSizeOverrides = Map.identity(); @@ -4207,6 +4307,8 @@ class RenderFlexLayout extends RenderLayoutBox { for (int childIndex = 0; childIndex < children.length; childIndex++) { final RenderBox child = children[childIndex]; final BoxConstraints childConstraints = _getIntrinsicConstraints(child); + final RenderBoxModel? effectiveChild = + child is RenderBoxModel ? child : null; final bool isMetricsOnlyMeasureChild = allowAnonymousMetricsOnlyCache && _isMetricsOnlyIntrinsicMeasureChild(child); @@ -4242,7 +4344,13 @@ class RenderFlexLayout extends RenderLayoutBox { _childrenRequirePostMeasureLayout[child] = true; } } else { - _layoutChildForFlex(child, childConstraints); + if (!_canReuseCurrentFlexMeasurement( + child, + childConstraints, + effectiveChild: effectiveChild, + )) { + _layoutChildForFlex(child, childConstraints); + } if (isMetricsOnlyMeasureChild && renderStyle.flexWrap == FlexWrap.nowrap && !_hasWrappingFlexAncestor()) { @@ -4251,8 +4359,8 @@ class RenderFlexLayout extends RenderLayoutBox { ); } - if (child is RenderBoxModel) { - child.clearOverrideContentSize(); + if (effectiveChild != null) { + effectiveChild.clearOverrideContentSize(); } childSize = Size.copy(_getChildSize(child)!); @@ -4370,6 +4478,7 @@ class RenderFlexLayout extends RenderLayoutBox { intrinsicMain, flowChild: cacheLookup.flowChild, reusableStateSignature: cacheLookup.reusableStateSignature, + sharedPlainTextSignature: cacheLookup.sharedPlainTextSignature, ); } @@ -4378,8 +4487,6 @@ class RenderFlexLayout extends RenderLayoutBox { _childrenIntrinsicMainSizes[child] = intrinsicMain; Size? intrinsicChildSize = _getChildSize(child, shouldUseIntrinsicMainSize: true); - final RenderBoxModel? effectiveChild = - child is RenderBoxModel ? child : null; final double? usedFlexBasis = effectiveChild != null ? _getUsedFlexBasis(child) : null; final _RunChild runChild = _createRunChildMetadata( @@ -4511,6 +4618,7 @@ class RenderFlexLayout extends RenderLayoutBox { } finally { _transientChildSizeOverrides = null; _metricsOnlyIntrinsicMeasureChildEligibilityCache = null; + _cacheableIntrinsicMeasureFlowChildCache = null; } if (runChildren.isNotEmpty) { diff --git a/webf/lib/src/rendering/flow.dart b/webf/lib/src/rendering/flow.dart index c3d3c33d24..741dd39b97 100644 --- a/webf/lib/src/rendering/flow.dart +++ b/webf/lib/src/rendering/flow.dart @@ -18,6 +18,28 @@ import 'package:webf/html.dart'; import 'package:webf/foundation.dart'; import 'package:webf/rendering.dart'; +bool _canReuseFlowChildLayout(RenderBox child, BoxConstraints constraints) { + if (!child.hasSize || child.constraints != constraints) { + return false; + } + if (child.debugNeedsLayout) { + return false; + } + if (child is RenderTextBox && child.hasPendingTextLayoutUpdate) { + return false; + } + if (child is RenderBoxModel && + (child.needsRelayout || + child.hasPendingSubtreeIntrinsicMeasurementInvalidation)) { + return false; + } + return true; +} + +bool _shouldAvoidParentUsesSizeForFlowChild(RenderBox child) { + return child is RenderEventListener || child is RenderLayoutBoxWrapper; +} + // Position and size of each run (line box) in flow layout. // https://www.w3.org/TR/css-inline-3/#line-boxes class RunMetrics { @@ -1175,8 +1197,20 @@ class RenderFlowLayout extends RenderLayoutBox { childConstraints = constraints; } - bool parentUseSize = !(child is RenderBoxModel && child.isSizeTight || child is RenderPositionPlaceholder); - child.layout(childConstraints, parentUsesSize: parentUseSize); + final bool avoidParentUsesSize = + _shouldAvoidParentUsesSizeForFlowChild(child); + if (child is RenderBoxModel) { + child.setRelayoutParentOnSizeChange( + avoidParentUsesSize ? this : null, + ); + } + bool parentUseSize = + !((child is RenderBoxModel && child.isSizeTight) || + child is RenderPositionPlaceholder || + avoidParentUsesSize); + if (!_canReuseFlowChildLayout(child, childConstraints)) { + child.layout(childConstraints, parentUsesSize: parentUseSize); + } double childMainAxisExtent = RenderFlowLayout.getPureMainAxisExtent(child); double childCrossAxisExtent = _getCrossAxisExtent(child); From cd30747e85e5bdb3c518a514e6ff086d014a3a16 Mon Sep 17 00:00:00 2001 From: andycall Date: Sat, 28 Mar 2026 07:08:45 -0700 Subject: [PATCH 11/13] perf(webf): optimize flex hotspot profiling --- .../profile_hotspot_cases_test.dart | 51 +- .../profile_hotspot_cases_test.dart | 29 +- webf/lib/src/rendering/flex.dart | 1907 +++++++++++------ 3 files changed, 1292 insertions(+), 695 deletions(-) diff --git a/integration_tests/integration_test/profile_hotspot_cases_test.dart b/integration_tests/integration_test/profile_hotspot_cases_test.dart index 1f3ed0b00b..cc7ec25b4a 100644 --- a/integration_tests/integration_test/profile_hotspot_cases_test.dart +++ b/integration_tests/integration_test/profile_hotspot_cases_test.dart @@ -658,22 +658,24 @@ void main() { widths: const ['378px', '344px', '316px', '356px'], ); - binding.reportData!['payment_method_otc_source_sheet_cpu_samples'] = - await _captureCpuSamples( - userTag: _paymentMethodOtcSourceSheetProfileTag, - action: () async { - await binding.traceAction( - () async { + late Map otcSourceSheetCpuSamples; + await binding.traceAction( + () async { + otcSourceSheetCpuSamples = await _captureCpuSamples( + userTag: _paymentMethodOtcSourceSheetProfileTag, + action: () async { await _runPaymentMethodOtcSourceSheetLoop( prepared, mutationIterations: 4, widths: const ['378px', '344px', '316px', '356px'], ); }, - reportKey: 'payment_method_otc_source_sheet_timeline', ); }, + reportKey: 'payment_method_otc_source_sheet_timeline', ); + binding.reportData!['payment_method_otc_source_sheet_cpu_samples'] = + otcSourceSheetCpuSamples; expect(host.getBoundingClientRect().width, greaterThan(0)); expect(sheet.getBoundingClientRect().height, greaterThan(0)); @@ -1032,17 +1034,36 @@ Future> _captureCpuSamples({ required developer.UserTag userTag, }) async { if (_shouldCaptureCpuSamplesOnDriverSide()) { - final developer.UserTag previousTag = userTag.makeCurrent(); + final developer.ServiceProtocolInfo info = await developer.Service.getInfo(); + final Uri? serviceUri = info.serverUri; + if (serviceUri == null) { + throw StateError('VM service URI is unavailable.'); + } + + final String vmServiceAddress = + 'ws://localhost:${serviceUri.port}${serviceUri.path}ws'; + final vm.VmService service = await vmServiceConnectUri(vmServiceAddress); try { - await action(); + final int startMicros = (await service.getVMTimelineMicros()).timestamp!; + final developer.UserTag previousTag = userTag.makeCurrent(); + try { + await action(); + } finally { + previousTag.makeCurrent(); + } + final int endMicros = (await service.getVMTimelineMicros()).timestamp!; + final int timeExtentMicros = + endMicros > startMicros ? endMicros - startMicros : 1; + + return { + 'captureMode': 'driver', + 'profileLabel': userTag.label, + 'timeOriginMicros': startMicros, + 'timeExtentMicros': timeExtentMicros, + }; } finally { - previousTag.makeCurrent(); + await service.dispose(); } - - return { - 'captureMode': 'driver', - 'profileLabel': userTag.label, - }; } final developer.ServiceProtocolInfo info = await developer.Service.getInfo(); diff --git a/integration_tests/test_driver/profile_hotspot_cases_test.dart b/integration_tests/test_driver/profile_hotspot_cases_test.dart index ef84ed76f4..a6fe222bdf 100644 --- a/integration_tests/test_driver/profile_hotspot_cases_test.dart +++ b/integration_tests/test_driver/profile_hotspot_cases_test.dart @@ -81,16 +81,25 @@ Future _materializeDriverCpuSamples( return; } - final int timeExtentMicros = + final int fallbackTimeExtentMicros = endMicros > startMicros ? endMicros - startMicros : 1; - final Map allSamples = (await driver.serviceClient - .getCpuSamples(driver.appIsolate.id!, startMicros, timeExtentMicros)) - .toJson(); - final List allSampleEntries = - (allSamples['samples'] as List? ?? []); for (final MapEntry> pendingCpuCapture in pendingCpuCaptures) { + final int captureStartMicros = + pendingCpuCapture.value['timeOriginMicros'] as int? ?? startMicros; + final int captureTimeExtentMicros = + pendingCpuCapture.value['timeExtentMicros'] as int? ?? + fallbackTimeExtentMicros; + final Map allSamples = (await driver.serviceClient + .getCpuSamples( + driver.appIsolate.id!, + captureStartMicros, + captureTimeExtentMicros, + )) + .toJson(); + final List allSampleEntries = + (allSamples['samples'] as List? ?? []); final String profileLabel = pendingCpuCapture.value['profileLabel'] as String? ?? ''; final List filteredSamples = allSampleEntries @@ -102,15 +111,15 @@ Future _materializeDriverCpuSamples( final Map filteredCpuSamples = Map.from(allSamples); filteredCpuSamples['sampleCount'] = filteredSamples.length; - filteredCpuSamples['timeOriginMicros'] = startMicros; - filteredCpuSamples['timeExtentMicros'] = timeExtentMicros; + filteredCpuSamples['timeOriginMicros'] = captureStartMicros; + filteredCpuSamples['timeExtentMicros'] = captureTimeExtentMicros; filteredCpuSamples['samples'] = filteredSamples; responseData[pendingCpuCapture.key] = { 'profileLabel': profileLabel, 'isolateId': driver.appIsolate.id, - 'timeOriginMicros': startMicros, - 'timeExtentMicros': timeExtentMicros, + 'timeOriginMicros': captureStartMicros, + 'timeExtentMicros': captureTimeExtentMicros, 'samples': filteredCpuSamples, }; } diff --git a/webf/lib/src/rendering/flex.dart b/webf/lib/src/rendering/flex.dart index f4d7a9f2ea..ebe84039c3 100644 --- a/webf/lib/src/rendering/flex.dart +++ b/webf/lib/src/rendering/flex.dart @@ -20,8 +20,7 @@ import 'package:webf/src/html/semantics_text.dart' show SpanElement; import 'package:webf/src/html/text.dart'; import 'package:webf/widget.dart'; -const bool _enableFlexProfileSections = - !kReleaseMode && +const bool _enableFlexProfileSections = !kReleaseMode && bool.fromEnvironment('WEBF_ENABLE_LAYOUT_PROFILE_SECTIONS'); enum _FlexFastPathRejectReason { @@ -307,7 +306,8 @@ class _FlexAdjustFastPathProfiler { ..sort((MapEntry a, MapEntry b) => b.value.compareTo(a.value)); return entries - .map((MapEntry entry) => '${labelFor(entry.key)}=${entry.value}') + .map( + (MapEntry entry) => '${labelFor(entry.key)}=${entry.value}') .join(', '); } @@ -439,7 +439,8 @@ class _FlexAnonymousMetricsProfiler { } static int get _maxDetailLogs { - final int configured = DebugFlags.flexAnonymousMetricsProfilingMaxDetailLogs; + final int configured = + DebugFlags.flexAnonymousMetricsProfilingMaxDetailLogs; return configured >= 0 ? configured : 0; } @@ -674,14 +675,15 @@ class _FlexIntrinsicMeasurementLookupResult { // Position and size info of each run (flex line) in flex layout. // https://www.w3.org/TR/css-flexbox-1/#flex-lines class _RunMetrics { - _RunMetrics(this.mainAxisExtent, - this.crossAxisExtent, - double totalFlexGrow, - double totalFlexShrink, - this.baselineExtent, - this.runChildren, - double remainingFreeSpace,) - : _totalFlexGrow = totalFlexGrow, + _RunMetrics( + this.mainAxisExtent, + this.crossAxisExtent, + double totalFlexGrow, + double totalFlexShrink, + this.baselineExtent, + this.runChildren, + double remainingFreeSpace, + ) : _totalFlexGrow = totalFlexGrow, _totalFlexShrink = totalFlexShrink, _remainingFreeSpace = remainingFreeSpace; @@ -730,32 +732,32 @@ class _RunMetrics { // Infos about flex item in the run. class _RunChild { - _RunChild(RenderBox child, - double originalMainSize, - double flexedMainSize, - bool frozen, { - required this.effectiveChild, - required this.alignSelf, - required this.flexGrow, - required this.flexShrink, - required this.usedFlexBasis, - required this.mainAxisMargin, - required this.mainAxisStartMargin, - required this.mainAxisEndMargin, - required this.crossAxisStartMargin, - required this.crossAxisEndMargin, - required this.hasAutoMainAxisMargin, - required this.hasAutoCrossAxisMargin, - required this.marginLeftAuto, - required this.marginRightAuto, - required this.marginTopAuto, - required this.marginBottomAuto, - required this.isReplaced, - required this.aspectRatio, - required this.mainAxisExtentAdjustment, - required this.crossAxisExtentAdjustment, - }) - : _child = child, + _RunChild( + RenderBox child, + double originalMainSize, + double flexedMainSize, + bool frozen, { + required this.effectiveChild, + required this.alignSelf, + required this.flexGrow, + required this.flexShrink, + required this.usedFlexBasis, + required this.mainAxisMargin, + required this.mainAxisStartMargin, + required this.mainAxisEndMargin, + required this.crossAxisStartMargin, + required this.crossAxisEndMargin, + required this.hasAutoMainAxisMargin, + required this.hasAutoCrossAxisMargin, + required this.marginLeftAuto, + required this.marginRightAuto, + required this.marginTopAuto, + required this.marginBottomAuto, + required this.isReplaced, + required this.aspectRatio, + required this.mainAxisExtentAdjustment, + required this.crossAxisExtentAdjustment, + }) : _child = child, _originalMainSize = originalMainSize, _flexedMainSize = flexedMainSize, _unclampedMainSize = originalMainSize, @@ -924,7 +926,8 @@ class _FlexContainerInvariants { factory _FlexContainerInvariants.compute(RenderFlexLayout layout) { final bool isHorizontalFlexDirection = layout._isHorizontalFlexDirection; - final bool isMainAxisStartAtPhysicalStart = layout._isMainAxisStartAtPhysicalStart(); + final bool isMainAxisStartAtPhysicalStart = + layout._isMainAxisStartAtPhysicalStart(); final bool isMainAxisReversed = layout._isMainAxisReversed(); // Determine cross axis orientation and where cross-start maps physically. @@ -933,7 +936,8 @@ class _FlexContainerInvariants { final FlexDirection flexDirection = layout.renderStyle.flexDirection; final bool isCrossAxisHorizontal; final bool isCrossAxisStartAtPhysicalStart; - if (flexDirection == FlexDirection.row || flexDirection == FlexDirection.rowReverse) { + if (flexDirection == FlexDirection.row || + flexDirection == FlexDirection.rowReverse) { // Cross is block axis. isCrossAxisHorizontal = !inlineIsHorizontal; if (isCrossAxisHorizontal) { @@ -948,7 +952,8 @@ class _FlexContainerInvariants { isCrossAxisHorizontal = inlineIsHorizontal; if (isCrossAxisHorizontal) { // Inline-start follows text direction in horizontal-tb. - isCrossAxisStartAtPhysicalStart = (layout.renderStyle.direction != TextDirection.rtl); + isCrossAxisStartAtPhysicalStart = + (layout.renderStyle.direction != TextDirection.rtl); } else { // Inline-start is physical top in vertical writing modes. isCrossAxisStartAtPhysicalStart = true; @@ -1080,15 +1085,18 @@ class RenderFlexLayout extends RenderLayoutBox { } final bool mainAxisIsHorizontal = _isHorizontalFlexDirection; - final double gap = _intrinsicMainAxisGap(mainAxisIsHorizontal: mainAxisIsHorizontal); + final double gap = + _intrinsicMainAxisGap(mainAxisIsHorizontal: mainAxisIsHorizontal); double contentWidth = 0.0; int count = 0; RenderBox? child = firstChild; while (child != null) { - final RenderLayoutParentData childParentData = child.parentData as RenderLayoutParentData; + final RenderLayoutParentData childParentData = + child.parentData as RenderLayoutParentData; if (child is! RenderPositionPlaceholder) { - final double w = child.getMinIntrinsicWidth(height) + _childMarginHorizontal(child); + final double w = + child.getMinIntrinsicWidth(height) + _childMarginHorizontal(child); if (mainAxisIsHorizontal) { if (w.isFinite) contentWidth += w; } else { @@ -1112,15 +1120,18 @@ class RenderFlexLayout extends RenderLayoutBox { } final bool mainAxisIsHorizontal = _isHorizontalFlexDirection; - final double gap = _intrinsicMainAxisGap(mainAxisIsHorizontal: mainAxisIsHorizontal); + final double gap = + _intrinsicMainAxisGap(mainAxisIsHorizontal: mainAxisIsHorizontal); double contentWidth = 0.0; int count = 0; RenderBox? child = firstChild; while (child != null) { - final RenderLayoutParentData childParentData = child.parentData as RenderLayoutParentData; + final RenderLayoutParentData childParentData = + child.parentData as RenderLayoutParentData; if (child is! RenderPositionPlaceholder) { - final double w = child.getMaxIntrinsicWidth(height) + _childMarginHorizontal(child); + final double w = + child.getMaxIntrinsicWidth(height) + _childMarginHorizontal(child); if (mainAxisIsHorizontal) { if (w.isFinite) contentWidth += w; } else { @@ -1144,15 +1155,18 @@ class RenderFlexLayout extends RenderLayoutBox { } final bool mainAxisIsHorizontal = _isHorizontalFlexDirection; - final double gap = _intrinsicMainAxisGap(mainAxisIsHorizontal: !mainAxisIsHorizontal); + final double gap = + _intrinsicMainAxisGap(mainAxisIsHorizontal: !mainAxisIsHorizontal); double contentHeight = 0.0; int count = 0; RenderBox? child = firstChild; while (child != null) { - final RenderLayoutParentData childParentData = child.parentData as RenderLayoutParentData; + final RenderLayoutParentData childParentData = + child.parentData as RenderLayoutParentData; if (child is! RenderPositionPlaceholder) { - final double h = child.getMinIntrinsicHeight(width) + _childMarginVertical(child); + final double h = + child.getMinIntrinsicHeight(width) + _childMarginVertical(child); if (!mainAxisIsHorizontal) { if (h.isFinite) contentHeight += h; } else { @@ -1176,15 +1190,18 @@ class RenderFlexLayout extends RenderLayoutBox { } final bool mainAxisIsHorizontal = _isHorizontalFlexDirection; - final double gap = _intrinsicMainAxisGap(mainAxisIsHorizontal: !mainAxisIsHorizontal); + final double gap = + _intrinsicMainAxisGap(mainAxisIsHorizontal: !mainAxisIsHorizontal); double contentHeight = 0.0; int count = 0; RenderBox? child = firstChild; while (child != null) { - final RenderLayoutParentData childParentData = child.parentData as RenderLayoutParentData; + final RenderLayoutParentData childParentData = + child.parentData as RenderLayoutParentData; if (child is! RenderPositionPlaceholder) { - final double h = child.getMaxIntrinsicHeight(width) + _childMarginVertical(child); + final double h = + child.getMaxIntrinsicHeight(width) + _childMarginVertical(child); if (!mainAxisIsHorizontal) { if (h.isFinite) contentHeight += h; } else { @@ -1210,10 +1227,13 @@ class RenderFlexLayout extends RenderLayoutBox { // Unwrap wrappers to read padding/border from the flex item element itself. RenderBoxModel? box = child is RenderBoxModel ? child - : (child is RenderEventListener ? child.child as RenderBoxModel? : null); + : (child is RenderEventListener + ? child.child as RenderBoxModel? + : null); if (box == null) return basis; final double paddingBorder = _isHorizontalFlexDirection - ? (box.renderStyle.padding.horizontal + box.renderStyle.border.horizontal) + ? (box.renderStyle.padding.horizontal + + box.renderStyle.border.horizontal) : (box.renderStyle.padding.vertical + box.renderStyle.border.vertical); return math.max(basis, paddingBorder); } @@ -1228,9 +1248,11 @@ class RenderFlexLayout extends RenderLayoutBox { Map.identity(); // Cache original constraints of children on the first layout. - Expando _childrenOldConstraints = Expando('childrenOldConstraints'); + Expando _childrenOldConstraints = + Expando('childrenOldConstraints'); Expando<_FlexIntrinsicMeasurementCacheBucket> _childrenIntrinsicMeasureCache = - Expando<_FlexIntrinsicMeasurementCacheBucket>('childrenIntrinsicMeasureCache'); + Expando<_FlexIntrinsicMeasurementCacheBucket>( + 'childrenIntrinsicMeasureCache'); final Map _childrenRequirePostMeasureLayout = Map.identity(); int _adjustedConstraintsCachePassId = -1; @@ -1240,6 +1262,8 @@ class RenderFlexLayout extends RenderLayoutBox { Map? _transientChildSizeOverrides; Map? _metricsOnlyIntrinsicMeasureChildEligibilityCache; Map? _cacheableIntrinsicMeasureFlowChildCache; + int _wrappingFlexAncestorCachePassId = -1; + bool? _hasWrappingFlexAncestorCached; int _reusableIntrinsicStyleSignaturePassId = -1; final Map _cachedReusableIntrinsicStyleSignatures = Map.identity(); @@ -1255,13 +1279,16 @@ class RenderFlexLayout extends RenderLayoutBox { _childrenIntrinsicMainSizes.clear(); _childrenOldConstraints = Expando('childrenOldConstraints'); _childrenIntrinsicMeasureCache = - Expando<_FlexIntrinsicMeasurementCacheBucket>('childrenIntrinsicMeasureCache'); + Expando<_FlexIntrinsicMeasurementCacheBucket>( + 'childrenIntrinsicMeasureCache'); _childrenRequirePostMeasureLayout.clear(); _adjustedConstraintsCachePassId = -1; _adjustedConstraintsCache.clear(); _transientChildSizeOverrides = null; _metricsOnlyIntrinsicMeasureChildEligibilityCache = null; _cacheableIntrinsicMeasureFlowChildCache = null; + _wrappingFlexAncestorCachePassId = -1; + _hasWrappingFlexAncestorCached = null; _reusableIntrinsicStyleSignaturePassId = -1; _cachedReusableIntrinsicStyleSignatures.clear(); } @@ -1310,7 +1337,8 @@ class RenderFlexLayout extends RenderLayoutBox { switch (renderStyle.flexDirection) { case FlexDirection.row: if (inlineIsHorizontal) { - return dir != TextDirection.rtl; // LTR → left is start; RTL → right is start + return dir != + TextDirection.rtl; // LTR → left is start; RTL → right is start } else { return true; // vertical inline: top is start } @@ -1321,10 +1349,10 @@ class RenderFlexLayout extends RenderLayoutBox { return false; // vertical inline: bottom is start } case FlexDirection.column: - // Column follows block axis. - // - horizontal-tb: block is vertical (top is start) - // - vertical-rl: block is horizontal (start at physical right) - // - vertical-lr: block is horizontal (start at physical left) + // Column follows block axis. + // - horizontal-tb: block is vertical (top is start) + // - vertical-rl: block is horizontal (start at physical right) + // - vertical-lr: block is horizontal (start at physical left) if (inlineIsHorizontal) { return true; // top is start } else { @@ -1357,20 +1385,29 @@ class RenderFlexLayout extends RenderLayoutBox { // Get start/end padding in the main axis according to flex direction. double _flowAwareMainAxisPadding({bool isEnd = false}) { final _FlexContainerInvariants? inv = _layoutInvariants; - if (inv != null) return isEnd ? inv.mainAxisPaddingEnd : inv.mainAxisPaddingStart; + if (inv != null) + return isEnd ? inv.mainAxisPaddingEnd : inv.mainAxisPaddingStart; if (_isHorizontalFlexDirection) { final bool startIsLeft = _isMainAxisStartAtPhysicalStart(); if (!isEnd) { - return startIsLeft ? renderStyle.paddingLeft.computedValue : renderStyle.paddingRight.computedValue; + return startIsLeft + ? renderStyle.paddingLeft.computedValue + : renderStyle.paddingRight.computedValue; } else { - return startIsLeft ? renderStyle.paddingRight.computedValue : renderStyle.paddingLeft.computedValue; + return startIsLeft + ? renderStyle.paddingRight.computedValue + : renderStyle.paddingLeft.computedValue; } } else { final bool startIsTop = _isMainAxisStartAtPhysicalStart(); if (!isEnd) { - return startIsTop ? renderStyle.paddingTop.computedValue : renderStyle.paddingBottom.computedValue; + return startIsTop + ? renderStyle.paddingTop.computedValue + : renderStyle.paddingBottom.computedValue; } else { - return startIsTop ? renderStyle.paddingBottom.computedValue : renderStyle.paddingTop.computedValue; + return startIsTop + ? renderStyle.paddingBottom.computedValue + : renderStyle.paddingTop.computedValue; } } } @@ -1378,24 +1415,28 @@ class RenderFlexLayout extends RenderLayoutBox { // Get start/end padding in the cross axis according to flex direction. double _flowAwareCrossAxisPadding({bool isEnd = false}) { final _FlexContainerInvariants? inv = _layoutInvariants; - if (inv != null) return isEnd ? inv.crossAxisPaddingEnd : inv.crossAxisPaddingStart; + if (inv != null) + return isEnd ? inv.crossAxisPaddingEnd : inv.crossAxisPaddingStart; // Cross axis comes from block axis for row, inline axis for column final CSSWritingMode wm = renderStyle.writingMode; final bool inlineIsHorizontal = (wm == CSSWritingMode.horizontalTb); final bool crossIsHorizontal; bool crossStartIsPhysicalStart; // left for horizontal, top for vertical - if (renderStyle.flexDirection == FlexDirection.row || renderStyle.flexDirection == FlexDirection.rowReverse) { + if (renderStyle.flexDirection == FlexDirection.row || + renderStyle.flexDirection == FlexDirection.rowReverse) { crossIsHorizontal = !inlineIsHorizontal; // block axis if (crossIsHorizontal) { - crossStartIsPhysicalStart = - (wm == CSSWritingMode.verticalLr); // start at left for vertical-lr, right for vertical-rl + crossStartIsPhysicalStart = (wm == + CSSWritingMode + .verticalLr); // start at left for vertical-lr, right for vertical-rl } else { crossStartIsPhysicalStart = true; // top for horizontal-tb } } else { crossIsHorizontal = inlineIsHorizontal; // inline axis if (crossIsHorizontal) { - crossStartIsPhysicalStart = (renderStyle.direction != TextDirection.rtl); // left if LTR, right if RTL + crossStartIsPhysicalStart = (renderStyle.direction != + TextDirection.rtl); // left if LTR, right if RTL } else { crossStartIsPhysicalStart = true; // top in vertical writing modes } @@ -1412,14 +1453,17 @@ class RenderFlexLayout extends RenderLayoutBox { : renderStyle.paddingLeft.computedValue; } } else { - return isEnd ? renderStyle.paddingBottom.computedValue : renderStyle.paddingTop.computedValue; + return isEnd + ? renderStyle.paddingBottom.computedValue + : renderStyle.paddingTop.computedValue; } } // Get start/end border in the main axis according to flex direction. double _flowAwareMainAxisBorder({bool isEnd = false}) { final _FlexContainerInvariants? inv = _layoutInvariants; - if (inv != null) return isEnd ? inv.mainAxisBorderEnd : inv.mainAxisBorderStart; + if (inv != null) + return isEnd ? inv.mainAxisBorderEnd : inv.mainAxisBorderStart; if (_isHorizontalFlexDirection) { final bool startIsLeft = _isMainAxisStartAtPhysicalStart(); if (!isEnd) { @@ -1448,13 +1492,16 @@ class RenderFlexLayout extends RenderLayoutBox { // Get start/end border in the cross axis according to flex direction. double _flowAwareCrossAxisBorder({bool isEnd = false}) { final _FlexContainerInvariants? inv = _layoutInvariants; - if (inv != null) return isEnd ? inv.crossAxisBorderEnd : inv.crossAxisBorderStart; + if (inv != null) + return isEnd ? inv.crossAxisBorderEnd : inv.crossAxisBorderStart; final CSSWritingMode wm = renderStyle.writingMode; final bool crossIsHorizontal = !_isHorizontalFlexDirection; if (crossIsHorizontal) { - final bool usesBlockAxis = renderStyle.flexDirection == FlexDirection.row || - renderStyle.flexDirection == FlexDirection.rowReverse; - final bool crossStartIsPhysicalLeft = usesBlockAxis ? (wm == CSSWritingMode.verticalLr) : true; + final bool usesBlockAxis = + renderStyle.flexDirection == FlexDirection.row || + renderStyle.flexDirection == FlexDirection.rowReverse; + final bool crossStartIsPhysicalLeft = + usesBlockAxis ? (wm == CSSWritingMode.verticalLr) : true; if (!isEnd) { return crossStartIsPhysicalLeft ? renderStyle.effectiveBorderLeftWidth.computedValue @@ -1509,14 +1556,16 @@ class RenderFlexLayout extends RenderLayoutBox { } double _calculateMainAxisMarginForJustContentType(double margin) { - if (renderStyle.justifyContent == JustifyContent.spaceBetween && margin < 0) { + if (renderStyle.justifyContent == JustifyContent.spaceBetween && + margin < 0) { return margin / 2; } return margin; } // Get start/end margin of child in the cross axis according to flex direction. - double? _flowAwareChildCrossAxisMargin(RenderBox child, {bool isEnd = false}) { + double? _flowAwareChildCrossAxisMargin(RenderBox child, + {bool isEnd = false}) { RenderBoxModel? childRenderBoxModel; if (child is RenderBoxModel) { childRenderBoxModel = child; @@ -1530,8 +1579,9 @@ class RenderFlexLayout extends RenderLayoutBox { final CSSWritingMode wm = renderStyle.writingMode; final bool crossIsHorizontal = !_isHorizontalFlexDirection; if (crossIsHorizontal) { - final bool usesBlockAxis = renderStyle.flexDirection == FlexDirection.row || - renderStyle.flexDirection == FlexDirection.rowReverse; + final bool usesBlockAxis = + renderStyle.flexDirection == FlexDirection.row || + renderStyle.flexDirection == FlexDirection.rowReverse; // When the cross axis is horizontal, it can be either the block axis (in // vertical writing modes for row/row-reverse) or the inline axis (for // column/column-reverse in horizontal writing mode). Inline-start depends @@ -1573,7 +1623,9 @@ class RenderFlexLayout extends RenderLayoutBox { } RenderBoxModel? box = child is RenderBoxModel ? child - : (child is RenderEventListener ? child.child as RenderBoxModel? : null); + : (child is RenderEventListener + ? child.child as RenderBoxModel? + : null); return box != null ? box.renderStyle.flexGrow : 0.0; } @@ -1584,14 +1636,18 @@ class RenderFlexLayout extends RenderLayoutBox { } RenderBoxModel? box = child is RenderBoxModel ? child - : (child is RenderEventListener ? child.child as RenderBoxModel? : null); + : (child is RenderEventListener + ? child.child as RenderBoxModel? + : null); return box != null ? box.renderStyle.flexShrink : 0.0; } double? _getFlexBasis(RenderBox child) { RenderBoxModel? box = child is RenderBoxModel ? child - : (child is RenderEventListener ? child.child as RenderBoxModel? : null); + : (child is RenderEventListener + ? child.child as RenderBoxModel? + : null); if (box != null && box.renderStyle.flexBasis != CSSLengthValue.auto) { // flex-basis: content → base size is content-based; do not return a numeric value here if (box.renderStyle.flexBasis?.type == CSSLengthType.CONTENT) { @@ -1604,7 +1660,9 @@ class RenderFlexLayout extends RenderLayoutBox { /// and if that containing block’s size is indefinite, the used value for flex-basis is content. // Note: When flex-basis is 0%, it should remain 0, not be changed to minContentWidth // The commented code below was incorrectly setting flexBasis to minContentWidth for 0% values - if (flexBasis != null && flexBasis == 0 && box.renderStyle.flexBasis?.type == CSSLengthType.PERCENTAGE) { + if (flexBasis != null && + flexBasis == 0 && + box.renderStyle.flexBasis?.type == CSSLengthType.PERCENTAGE) { // CSS Flexbox: percentage flex-basis is resolved against the flex container’s // inner main size. If that size is indefinite, the used value is 'content'. // Consider explicit sizing, tight constraints, or bounded constraints as definite. @@ -1612,8 +1670,10 @@ class RenderFlexLayout extends RenderLayoutBox { ? (renderStyle.contentBoxLogicalWidth != null) : (renderStyle.contentBoxLogicalHeight != null); final bool mainTight = _isHorizontalFlexDirection - ? ((contentConstraints?.hasTightWidth ?? false) || constraints.hasTightWidth) - : ((contentConstraints?.hasTightHeight ?? false) || constraints.hasTightHeight); + ? ((contentConstraints?.hasTightWidth ?? false) || + constraints.hasTightWidth) + : ((contentConstraints?.hasTightHeight ?? false) || + constraints.hasTightHeight); final bool mainDefinite = hasSpecifiedMain || mainTight; if (!mainDefinite) { return null; @@ -1640,15 +1700,18 @@ class RenderFlexLayout extends RenderLayoutBox { double _getMaxMainAxisSize(RenderBoxModel child) { double? resolvePctCap(CSSLengthValue len) { - if (len.type != CSSLengthType.PERCENTAGE || len.value == null) return null; + if (len.type != CSSLengthType.PERCENTAGE || len.value == null) + return null; // Determine the container's inner content-box size in the main axis. double? containerInner; if (_isHorizontalFlexDirection) { containerInner = renderStyle.contentBoxLogicalWidth; if (containerInner == null) { - if (contentConstraints != null && contentConstraints!.maxWidth.isFinite) { + if (contentConstraints != null && + contentConstraints!.maxWidth.isFinite) { containerInner = contentConstraints!.maxWidth; - } else if (constraints.hasTightWidth && constraints.maxWidth.isFinite) { + } else if (constraints.hasTightWidth && + constraints.maxWidth.isFinite) { containerInner = constraints.maxWidth; } else { // Fallback to ancestor-provided available inline size used for shrink-to-fit. @@ -1659,9 +1722,11 @@ class RenderFlexLayout extends RenderLayoutBox { } else { containerInner = renderStyle.contentBoxLogicalHeight; if (containerInner == null) { - if (contentConstraints != null && contentConstraints!.maxHeight.isFinite) { + if (contentConstraints != null && + contentConstraints!.maxHeight.isFinite) { containerInner = contentConstraints!.maxHeight; - } else if (constraints.hasTightHeight && constraints.maxHeight.isFinite) { + } else if (constraints.hasTightHeight && + constraints.maxHeight.isFinite) { containerInner = constraints.maxHeight; } } @@ -1695,9 +1760,13 @@ class RenderFlexLayout extends RenderLayoutBox { if (child is RenderBoxModel) { double autoMinSize = _getAutoMinSize(child); if (_isHorizontalFlexDirection) { - minMainSize = child.renderStyle.minWidth.isAuto ? autoMinSize : child.renderStyle.minWidth.computedValue; + minMainSize = child.renderStyle.minWidth.isAuto + ? autoMinSize + : child.renderStyle.minWidth.computedValue; } else { - minMainSize = child.renderStyle.minHeight.isAuto ? autoMinSize : child.renderStyle.minHeight.computedValue; + minMainSize = child.renderStyle.minHeight.isAuto + ? autoMinSize + : child.renderStyle.minHeight.computedValue; } } @@ -1839,10 +1908,9 @@ class RenderFlexLayout extends RenderLayoutBox { return false; } - final bool currentSatisfiesApplied = - appliedMainMin <= currentMainSize + 0.5 && - (!appliedMainMax.isFinite || - currentMainSize <= appliedMainMax + 0.5); + final bool currentSatisfiesApplied = appliedMainMin <= + currentMainSize + 0.5 && + (!appliedMainMax.isFinite || currentMainSize <= appliedMainMax + 0.5); return currentSatisfiesApplied; } @@ -1876,13 +1944,16 @@ class RenderFlexLayout extends RenderLayoutBox { // (clamped by its max main size property if it’s definite). It is otherwise undefined. // https://www.w3.org/TR/css-flexbox-1/#specified-size-suggestion double? specifiedSize; - final CSSLengthValue mainSize = _isHorizontalFlexDirection ? childRenderStyle.width : childRenderStyle.height; + final CSSLengthValue mainSize = _isHorizontalFlexDirection + ? childRenderStyle.width + : childRenderStyle.height; if (!mainSize.isIntrinsic && mainSize.isNotAuto) { if (mainSize.type == CSSLengthType.PERCENTAGE) { // Percentage main sizes resolve against the flex container and may be // indefinite until layout. Use the already-resolved logical size when // available. - specifiedSize = _isHorizontalFlexDirection ? childLogicalWidth : childLogicalHeight; + specifiedSize = + _isHorizontalFlexDirection ? childLogicalWidth : childLogicalHeight; } else { // Avoid using `contentBoxLogicalWidth/Height` here: those values can be // overridden by flex sizing (grow/shrink) and would make the "specified @@ -1893,7 +1964,8 @@ class RenderFlexLayout extends RenderLayoutBox { double contentBoxMain = _isHorizontalFlexDirection ? childRenderStyle.deflatePaddingBorderWidth(borderBoxMain) : childRenderStyle.deflatePaddingBorderHeight(borderBoxMain); - if (!contentBoxMain.isFinite || contentBoxMain < 0) contentBoxMain = 0; + if (!contentBoxMain.isFinite || contentBoxMain < 0) + contentBoxMain = 0; specifiedSize = contentBoxMain; } } @@ -1925,8 +1997,10 @@ class RenderFlexLayout extends RenderLayoutBox { double contentSize; if (_isHorizontalFlexDirection) { if (child is RenderFlowLayout && child.inlineFormattingContext != null) { - final double ifcMin = child.inlineFormattingContext!.paragraphMinIntrinsicWidth; - contentSize = (ifcMin.isFinite && ifcMin > 0) ? ifcMin : child.minContentWidth; + final double ifcMin = + child.inlineFormattingContext!.paragraphMinIntrinsicWidth; + contentSize = + (ifcMin.isFinite && ifcMin > 0) ? ifcMin : child.minContentWidth; } else { contentSize = child.minContentWidth; } @@ -1941,7 +2015,9 @@ class RenderFlexLayout extends RenderLayoutBox { } } - CSSLengthValue childCrossSize = _isHorizontalFlexDirection ? childRenderStyle.height : childRenderStyle.width; + CSSLengthValue childCrossSize = _isHorizontalFlexDirection + ? childRenderStyle.height + : childRenderStyle.width; if (childCrossSize.isNotAuto && transferredSize != null) { contentSize = transferredSize; @@ -1954,15 +2030,19 @@ class RenderFlexLayout extends RenderLayoutBox { if (childAspectRatio != null) { if (_isHorizontalFlexDirection) { if (childRenderStyle.minHeight.isNotAuto) { - transferredMinSize = childRenderStyle.minHeight.computedValue * childAspectRatio; + transferredMinSize = + childRenderStyle.minHeight.computedValue * childAspectRatio; } else if (childRenderStyle.maxHeight.isNotNone) { - transferredMaxSize = childRenderStyle.maxHeight.computedValue * childAspectRatio; + transferredMaxSize = + childRenderStyle.maxHeight.computedValue * childAspectRatio; } } else if (!_isHorizontalFlexDirection) { if (childRenderStyle.minWidth.isNotAuto) { - transferredMinSize = childRenderStyle.minWidth.computedValue / childAspectRatio; + transferredMinSize = + childRenderStyle.minWidth.computedValue / childAspectRatio; } else if (childRenderStyle.maxWidth.isNotNone) { - transferredMaxSize = childRenderStyle.maxWidth.computedValue * childAspectRatio; + transferredMaxSize = + childRenderStyle.maxWidth.computedValue * childAspectRatio; } } } @@ -1975,19 +2055,23 @@ class RenderFlexLayout extends RenderLayoutBox { contentSize = transferredMaxSize; } - double? crossSize = - _isHorizontalFlexDirection ? renderStyle.contentBoxLogicalHeight : renderStyle.contentBoxLogicalWidth; + double? crossSize = _isHorizontalFlexDirection + ? renderStyle.contentBoxLogicalHeight + : renderStyle.contentBoxLogicalWidth; // Content size suggestion of replaced flex item will use the cross axis preferred size which came from flexbox's // fixed cross size in newer version of Blink and Gecko which is different from the behavior of WebKit. // https://github.com/w3c/csswg-drafts/issues/6693 - bool isChildCrossSizeStretched = _needToStretchChildCrossSize(child) && crossSize != null; + bool isChildCrossSizeStretched = + _needToStretchChildCrossSize(child) && crossSize != null; if (isChildCrossSizeStretched && transferredSize != null) { contentSize = transferredSize; } - CSSLengthValue maxMainLength = _isHorizontalFlexDirection ? childRenderStyle.maxWidth : childRenderStyle.maxHeight; + CSSLengthValue maxMainLength = _isHorizontalFlexDirection + ? childRenderStyle.maxWidth + : childRenderStyle.maxHeight; // Further clamped by the max main size property if that is definite. if (maxMainLength.isNotNone) { @@ -2019,17 +2103,22 @@ class RenderFlexLayout extends RenderLayoutBox { // Convert the content-box minimum to a border-box minimum by adding padding and border // on the flex container's main axis, per CSS sizing. final double paddingBorderMain = _isHorizontalFlexDirection - ? (childRenderStyle.padding.horizontal + childRenderStyle.border.horizontal) - : (childRenderStyle.padding.vertical + childRenderStyle.border.vertical); + ? (childRenderStyle.padding.horizontal + + childRenderStyle.border.horizontal) + : (childRenderStyle.padding.vertical + + childRenderStyle.border.vertical); // If overflow in the flex main axis is not visible, browsers allow the flex item // to shrink below the content-based minimum. Model this by treating the automatic // minimum as zero (border-box becomes just padding+border). - if ((_isHorizontalFlexDirection && childRenderStyle.overflowX != CSSOverflowType.visible) || - (!_isHorizontalFlexDirection && childRenderStyle.overflowY != CSSOverflowType.visible)) { + if ((_isHorizontalFlexDirection && + childRenderStyle.overflowX != CSSOverflowType.visible) || + (!_isHorizontalFlexDirection && + childRenderStyle.overflowY != CSSOverflowType.visible)) { double autoMinBorderBox = paddingBorderMain; if (maxMainLength.isNotNone) { - autoMinBorderBox = math.min(autoMinBorderBox, maxMainLength.computedValue); + autoMinBorderBox = + math.min(autoMinBorderBox, maxMainLength.computedValue); } return autoMinBorderBox; } @@ -2038,7 +2127,8 @@ class RenderFlexLayout extends RenderLayoutBox { // Finally, clamp by the definite max main size (which is border-box) if present. if (maxMainLength.isNotNone) { - autoMinBorderBox = math.min(autoMinBorderBox, maxMainLength.computedValue); + autoMinBorderBox = + math.min(autoMinBorderBox, maxMainLength.computedValue); } return autoMinBorderBox; @@ -2059,9 +2149,11 @@ class RenderFlexLayout extends RenderLayoutBox { child = child.child as RenderBoxModel; } if (_isPlaceholderPositioned(child)) { - RenderBoxModel? positionedBox = (child as RenderPositionPlaceholder).positioned; + RenderBoxModel? positionedBox = + (child as RenderPositionPlaceholder).positioned; if (positionedBox != null && positionedBox.hasSize == true) { - Size realDisplayedBoxSize = positionedBox.getBoxSize(positionedBox.contentSize); + Size realDisplayedBoxSize = + positionedBox.getBoxSize(positionedBox.contentSize); return BoxConstraints( minWidth: realDisplayedBoxSize.width, maxWidth: realDisplayedBoxSize.width, @@ -2094,7 +2186,8 @@ class RenderFlexLayout extends RenderLayoutBox { // This prevents items from measuring at full container width/height. // Exception: replaced elements (e.g., ) should not be relaxed to ∞, // otherwise they pick viewport-sized widths; keep their container-bounded constraints. - final bool isFlexBasisContent = s.flexBasis?.type == CSSLengthType.CONTENT; + final bool isFlexBasisContent = + s.flexBasis?.type == CSSLengthType.CONTENT; final bool isReplaced = s.isSelfRenderReplaced(); if (_isHorizontalFlexDirection) { // Row direction: main axis is width. For intrinsic measurement, avoid @@ -2103,12 +2196,12 @@ class RenderFlexLayout extends RenderLayoutBox { if (!isReplaced && (s.width.isAuto || isFlexBasisContent)) { // Relax the minimum width to the element's own border-box minimum, // not the parent-imposed tight width, so shrink-to-fit can occur. - double minBorderBoxW = - s.effectiveBorderLeftWidth.computedValue + - s.effectiveBorderRightWidth.computedValue + - s.paddingLeft.computedValue + - s.paddingRight.computedValue; - if (s.minWidth.isNotAuto && s.minWidth.type != CSSLengthType.PERCENTAGE) { + double minBorderBoxW = s.effectiveBorderLeftWidth.computedValue + + s.effectiveBorderRightWidth.computedValue + + s.paddingLeft.computedValue + + s.paddingRight.computedValue; + if (s.minWidth.isNotAuto && + s.minWidth.type != CSSLengthType.PERCENTAGE) { minBorderBoxW = math.max(minBorderBoxW, s.minWidth.computedValue); } c = BoxConstraints( @@ -2124,12 +2217,12 @@ class RenderFlexLayout extends RenderLayoutBox { // when the item has auto height or flex-basis:content. This lets the // item size to its content instead of being prematurely clamped to 0. if (!isReplaced && (s.height.isAuto || isFlexBasisContent)) { - double minBorderBoxH = - s.effectiveBorderTopWidth.computedValue + - s.effectiveBorderBottomWidth.computedValue + - s.paddingTop.computedValue + - s.paddingBottom.computedValue; - if (s.minHeight.isNotAuto && s.minHeight.type != CSSLengthType.PERCENTAGE) { + double minBorderBoxH = s.effectiveBorderTopWidth.computedValue + + s.effectiveBorderBottomWidth.computedValue + + s.paddingTop.computedValue + + s.paddingBottom.computedValue; + if (s.minHeight.isNotAuto && + s.minHeight.type != CSSLengthType.PERCENTAGE) { minBorderBoxH = math.max(minBorderBoxH, s.minHeight.computedValue); } c = BoxConstraints( @@ -2147,8 +2240,11 @@ class RenderFlexLayout extends RenderLayoutBox { if (!isReplaced && (s.width.isAuto || isFlexBasisContent)) { // Determine if child should be stretched in cross axis. final AlignSelf self = s.alignSelf; - final bool parentStretch = renderStyle.alignItems == AlignItems.stretch; - final bool shouldStretch = self == AlignSelf.auto ? parentStretch : self == AlignSelf.stretch; + final bool parentStretch = + renderStyle.alignItems == AlignItems.stretch; + final bool shouldStretch = self == AlignSelf.auto + ? parentStretch + : self == AlignSelf.stretch; // Determine if the container has a definite cross size (width). // Determine whether the flex container's cross size (width in column direction) @@ -2159,12 +2255,14 @@ class RenderFlexLayout extends RenderLayoutBox { // container is not inline-flex with auto width (inline-flex shrink-to-fit should // be treated as indefinite during intrinsic measurement). final bool isInlineFlexAuto = - renderStyle.effectiveDisplay == CSSDisplay.inlineFlex && renderStyle.width.isAuto; + renderStyle.effectiveDisplay == CSSDisplay.inlineFlex && + renderStyle.width.isAuto; final bool containerCrossDefinite = (renderStyle.contentBoxLogicalWidth != null) || (contentConstraints?.hasTightWidth ?? false) || (constraints.hasTightWidth && !isInlineFlexAuto) || - (((contentConstraints?.hasBoundedWidth ?? false) || constraints.hasBoundedWidth) && + (((contentConstraints?.hasBoundedWidth ?? false) || + constraints.hasBoundedWidth) && !isInlineFlexAuto); double newMaxW; @@ -2174,11 +2272,13 @@ class RenderFlexLayout extends RenderLayoutBox { double boundedContainerW = double.infinity; if (constraints.hasTightWidth && constraints.maxWidth.isFinite) { boundedContainerW = constraints.maxWidth; - } else if ((contentConstraints?.hasTightWidth ?? false) && contentConstraints!.maxWidth.isFinite) { + } else if ((contentConstraints?.hasTightWidth ?? false) && + contentConstraints!.maxWidth.isFinite) { boundedContainerW = contentConstraints!.maxWidth; } else if (!isInlineFlexAuto) { // Fall back to bounded (non-tight) width for block-level flex containers. - if ((contentConstraints?.hasBoundedWidth ?? false) && contentConstraints!.maxWidth.isFinite) { + if ((contentConstraints?.hasBoundedWidth ?? false) && + contentConstraints!.maxWidth.isFinite) { boundedContainerW = contentConstraints!.maxWidth; } } @@ -2195,7 +2295,8 @@ class RenderFlexLayout extends RenderLayoutBox { newMaxW = double.infinity; } // Also honor the child's own definite max-width (non-percentage) if any. - if (s.maxWidth.isNotNone && s.maxWidth.type != CSSLengthType.PERCENTAGE) { + if (s.maxWidth.isNotNone && + s.maxWidth.type != CSSLengthType.PERCENTAGE) { newMaxW = math.min(newMaxW, s.maxWidth.computedValue); } @@ -2230,7 +2331,9 @@ class RenderFlexLayout extends RenderLayoutBox { // when that size is effectively indefinite for intrinsic measurement, using 0 // would prematurely collapse the child. Let content sizing drive the measure. final RenderBoxModel childBox = child; - final bool isZeroPctBasis = childBox.renderStyle.flexBasis?.type == CSSLengthType.PERCENTAGE && basis == 0; + final bool isZeroPctBasis = + childBox.renderStyle.flexBasis?.type == CSSLengthType.PERCENTAGE && + basis == 0; // Only skip clamping to 0 for percentage flex-basis during intrinsic sizing // in column direction (vertical main axis). In row direction, a 0% flex-basis // should remain 0 for intrinsic measurement so width-based distribution matches CSS. @@ -2240,8 +2343,8 @@ class RenderFlexLayout extends RenderLayoutBox { // Flex-basis is a definite length. Honor box-sizing:border-box semantics: // the used border-box size cannot be smaller than padding+border. if (_isHorizontalFlexDirection) { - final double minBorderBoxW = - child.renderStyle.padding.horizontal + child.renderStyle.border.horizontal; + final double minBorderBoxW = child.renderStyle.padding.horizontal + + child.renderStyle.border.horizontal; final double used = math.max(basis, minBorderBoxW); c = BoxConstraints( minWidth: used, @@ -2250,8 +2353,8 @@ class RenderFlexLayout extends RenderLayoutBox { maxHeight: c.maxHeight, ); } else { - final double minBorderBoxH = - child.renderStyle.padding.vertical + child.renderStyle.border.vertical; + final double minBorderBoxH = child.renderStyle.padding.vertical + + child.renderStyle.border.vertical; final double used = math.max(basis, minBorderBoxH); c = BoxConstraints( minWidth: c.minWidth, @@ -2269,7 +2372,8 @@ class RenderFlexLayout extends RenderLayoutBox { } } - double _horizontalMarginNegativeSet(double baseSize, RenderBoxModel box, {bool isHorizontal = false}) { + double _horizontalMarginNegativeSet(double baseSize, RenderBoxModel box, + {bool isHorizontal = false}) { CSSRenderStyle boxStyle = box.renderStyle; double? marginLeft = boxStyle.marginLeft.computedValue; double? marginRight = boxStyle.marginRight.computedValue; @@ -2291,8 +2395,10 @@ class RenderFlexLayout extends RenderLayoutBox { return baseSize + box.renderStyle.margin.vertical; } - double _getMainSize(RenderBox child, {bool shouldUseIntrinsicMainSize = false}) { - Size? childSize = _getChildSize(child, shouldUseIntrinsicMainSize: shouldUseIntrinsicMainSize); + double _getMainSize(RenderBox child, + {bool shouldUseIntrinsicMainSize = false}) { + Size? childSize = _getChildSize(child, + shouldUseIntrinsicMainSize: shouldUseIntrinsicMainSize); if (_isHorizontalFlexDirection) { return childSize!.width; } else { @@ -2304,9 +2410,8 @@ class RenderFlexLayout extends RenderLayoutBox { double _getMainAxisGap() { final _FlexContainerInvariants? inv = _layoutInvariants; if (inv != null) return inv.mainAxisGap; - CSSLengthValue gap = _isHorizontalFlexDirection - ? renderStyle.columnGap - : renderStyle.rowGap; + CSSLengthValue gap = + _isHorizontalFlexDirection ? renderStyle.columnGap : renderStyle.rowGap; if (gap.type == CSSLengthType.NORMAL) return 0; return gap.computedValue; } @@ -2315,9 +2420,8 @@ class RenderFlexLayout extends RenderLayoutBox { double _getCrossAxisGap() { final _FlexContainerInvariants? inv = _layoutInvariants; if (inv != null) return inv.crossAxisGap; - CSSLengthValue gap = _isHorizontalFlexDirection - ? renderStyle.rowGap - : renderStyle.columnGap; + CSSLengthValue gap = + _isHorizontalFlexDirection ? renderStyle.rowGap : renderStyle.columnGap; if (gap.type == CSSLengthType.NORMAL) return 0; return gap.computedValue; } @@ -2357,9 +2461,12 @@ class RenderFlexLayout extends RenderLayoutBox { ); items.sort((_OrderedFlexItem a, _OrderedFlexItem b) { final int byOrder = a.order.compareTo(b.order); - return byOrder != 0 ? byOrder : a.originalIndex.compareTo(b.originalIndex); + return byOrder != 0 + ? byOrder + : a.originalIndex.compareTo(b.originalIndex); }); - return List.generate(items.length, (int i) => items[i].child, growable: false); + return List.generate(items.length, (int i) => items[i].child, + growable: false); } _FlexResolutionInputs _computeFlexResolutionInputs() { @@ -2376,24 +2483,28 @@ class RenderFlexLayout extends RenderLayoutBox { double? containerHeight; if (containerWidth == null) { - if ((contentConstraints?.hasBoundedWidth ?? false) && (contentConstraints?.maxWidth.isFinite ?? false)) { + if ((contentConstraints?.hasBoundedWidth ?? false) && + (contentConstraints?.maxWidth.isFinite ?? false)) { containerWidth = contentConstraints!.maxWidth; } else if (constraints.hasBoundedWidth && constraints.maxWidth.isFinite) { containerWidth = constraints.maxWidth; } } if (constraints.hasBoundedWidth && constraints.maxWidth.isFinite) { - containerWidth = - (containerWidth == null) ? constraints.maxWidth : math.min(containerWidth, constraints.maxWidth); + containerWidth = (containerWidth == null) + ? constraints.maxWidth + : math.min(containerWidth, constraints.maxWidth); } - if ((contentConstraints?.hasBoundedHeight ?? false) && (contentConstraints?.maxHeight.isFinite ?? false)) { + if ((contentConstraints?.hasBoundedHeight ?? false) && + (contentConstraints?.maxHeight.isFinite ?? false)) { containerHeight = contentConstraints!.maxHeight; } else if (constraints.hasBoundedHeight && constraints.maxHeight.isFinite) { containerHeight = constraints.maxHeight; } if (constraints.hasBoundedHeight && constraints.maxHeight.isFinite) { - containerHeight = - (containerHeight == null) ? constraints.maxHeight : math.min(containerHeight, constraints.maxHeight); + containerHeight = (containerHeight == null) + ? constraints.maxHeight + : math.min(containerHeight, constraints.maxHeight); } if (contentBoxLogicalHeight != null) { containerHeight = contentBoxLogicalHeight; @@ -2403,14 +2514,18 @@ class RenderFlexLayout extends RenderLayoutBox { final double? maxMainSize = isHorizontal ? containerWidth : containerHeight; final bool isMainSizeDefinite = isHorizontal - ? (contentBoxLogicalWidth != null || (contentConstraints?.hasTightWidth ?? false) || - constraints.hasTightWidth || - ((contentConstraints?.hasBoundedWidth ?? false) && (contentConstraints?.maxWidth.isFinite ?? false)) || - (constraints.hasBoundedWidth && constraints.maxWidth.isFinite)) - : (contentBoxLogicalHeight != null || (contentConstraints?.hasTightHeight ?? false) || - constraints.hasTightHeight || - ((contentConstraints?.hasBoundedHeight ?? false) && (contentConstraints?.maxHeight.isFinite ?? false)) || - (constraints.hasBoundedHeight && constraints.maxHeight.isFinite)); + ? (contentBoxLogicalWidth != null || + (contentConstraints?.hasTightWidth ?? false) || + constraints.hasTightWidth || + ((contentConstraints?.hasBoundedWidth ?? false) && + (contentConstraints?.maxWidth.isFinite ?? false)) || + (constraints.hasBoundedWidth && constraints.maxWidth.isFinite)) + : (contentBoxLogicalHeight != null || + (contentConstraints?.hasTightHeight ?? false) || + constraints.hasTightHeight || + ((contentConstraints?.hasBoundedHeight ?? false) && + (contentConstraints?.maxHeight.isFinite ?? false)) || + (constraints.hasBoundedHeight && constraints.maxHeight.isFinite)); return _FlexResolutionInputs( contentBoxLogicalWidth: contentBoxLogicalWidth, @@ -2421,9 +2536,11 @@ class RenderFlexLayout extends RenderLayoutBox { } _RunChild _createRunChildMetadata(RenderBox child, double originalMainSize, - {required RenderBoxModel? effectiveChild, required double? usedFlexBasis}) { - final RenderBoxModel? marginBoxModel = - child is RenderBoxModel ? child : (child is RenderPositionPlaceholder ? child.positioned : null); + {required RenderBoxModel? effectiveChild, + required double? usedFlexBasis}) { + final RenderBoxModel? marginBoxModel = child is RenderBoxModel + ? child + : (child is RenderPositionPlaceholder ? child.positioned : null); double marginHorizontal = 0.0; double marginVertical = 0.0; double mainAxisMargin = 0.0; @@ -2433,7 +2550,8 @@ class RenderFlexLayout extends RenderLayoutBox { marginHorizontal = s.marginLeft.computedValue + s.marginRight.computedValue; marginVertical = s.marginTop.computedValue + s.marginBottom.computedValue; - mainAxisMargin = _isHorizontalFlexDirection ? marginHorizontal : marginVertical; + mainAxisMargin = + _isHorizontalFlexDirection ? marginHorizontal : marginVertical; } final RenderStyle? marginStyle = marginBoxModel?.renderStyle; @@ -2488,9 +2606,11 @@ class RenderFlexLayout extends RenderLayoutBox { usedFlexBasis: usedFlexBasis, mainAxisMargin: mainAxisMargin, mainAxisStartMargin: _flowAwareChildMainAxisMargin(child) ?? 0.0, - mainAxisEndMargin: _flowAwareChildMainAxisMargin(child, isEnd: true) ?? 0.0, + mainAxisEndMargin: + _flowAwareChildMainAxisMargin(child, isEnd: true) ?? 0.0, crossAxisStartMargin: _flowAwareChildCrossAxisMargin(child) ?? 0.0, - crossAxisEndMargin: _flowAwareChildCrossAxisMargin(child, isEnd: true) ?? 0.0, + crossAxisEndMargin: + _flowAwareChildCrossAxisMargin(child, isEnd: true) ?? 0.0, hasAutoMainAxisMargin: hasAutoMainAxisMargin, hasAutoCrossAxisMargin: hasAutoCrossAxisMargin, marginLeftAuto: marginLeftAuto, @@ -2529,14 +2649,19 @@ class RenderFlexLayout extends RenderLayoutBox { return crossSize + runChild.crossAxisExtentAdjustment; } - void _cacheOriginalConstraintsIfNeeded(RenderBox child, BoxConstraints appliedConstraints) { + void _cacheOriginalConstraintsIfNeeded( + RenderBox child, BoxConstraints appliedConstraints) { RenderBoxModel? box = child is RenderBoxModel ? child - : (child is RenderEventListener ? child.child as RenderBoxModel? : null); + : (child is RenderEventListener + ? child.child as RenderBoxModel? + : null); if (box == null) return; - bool hasPercentageMaxWidth = box.renderStyle.maxWidth.type == CSSLengthType.PERCENTAGE; - bool hasPercentageMaxHeight = box.renderStyle.maxHeight.type == CSSLengthType.PERCENTAGE; + bool hasPercentageMaxWidth = + box.renderStyle.maxWidth.type == CSSLengthType.PERCENTAGE; + bool hasPercentageMaxHeight = + box.renderStyle.maxHeight.type == CSSLengthType.PERCENTAGE; if (hasPercentageMaxWidth || hasPercentageMaxHeight) { _childrenOldConstraints[box] = appliedConstraints; @@ -2570,8 +2695,7 @@ class RenderFlexLayout extends RenderLayoutBox { break; } - if (current is RenderEventListener || - current is RenderLayoutBoxWrapper) { + if (current is RenderEventListener || current is RenderLayoutBoxWrapper) { current = (current as dynamic).child as RenderBox?; depth++; continue; @@ -2602,7 +2726,8 @@ class RenderFlexLayout extends RenderLayoutBox { bool _shouldSkipEarlyNoFlexNoStretchNoBaselineRunMetrics( List children, ) { - if (!_isHorizontalFlexDirection || renderStyle.flexWrap != FlexWrap.nowrap) { + if (!_isHorizontalFlexDirection || + renderStyle.flexWrap != FlexWrap.nowrap) { return true; } @@ -2611,22 +2736,37 @@ class RenderFlexLayout extends RenderLayoutBox { continue; } + final RenderBoxModel? box = child is RenderBoxModel + ? child + : (child is RenderEventListener + ? child.child as RenderBoxModel? + : null); final double flexGrow = _getFlexGrow(child); final double flexShrink = _getFlexShrink(child); if (flexGrow <= 0 && flexShrink <= 0) { + final RenderFlowLayout? flowChild = + _getCacheableIntrinsicMeasureFlowChild( + child, + allowAnonymous: true, + ); + if (flowChild != null && + _isMetricsOnlyIntrinsicMeasureFlowChild(flowChild) && + (flowChild.needsRelayout || + (box?.needsRelayout ?? false) || + _subtreeHasPendingIntrinsicMeasureInvalidation(child))) { + return true; + } continue; } - final RenderBoxModel? box = child is RenderBoxModel - ? child - : (child is RenderEventListener ? child.child as RenderBoxModel? : null); if (box == null) { return true; } final double? flexBasis = _getFlexBasis(child); - final CSSLengthValue explicitMainSize = - _isHorizontalFlexDirection ? box.renderStyle.width : box.renderStyle.height; + final CSSLengthValue explicitMainSize = _isHorizontalFlexDirection + ? box.renderStyle.width + : box.renderStyle.height; if (flexBasis == null && explicitMainSize.isAuto) { return true; } @@ -2636,6 +2776,12 @@ class RenderFlexLayout extends RenderLayoutBox { } bool _canUseAnonymousMetricsOnlyCache(List children) { + if (_hasWrappingFlexAncestor()) { + _recordAnonymousMetricsReject( + _FlexAnonymousMetricsRejectReason.wrappedContainer, + ); + return false; + } int metricsOnlyChildCount = 0; int flexingMetricsOnlyChildCount = 0; for (int childIndex = 0; childIndex < children.length; childIndex++) { @@ -2710,7 +2856,8 @@ class RenderFlexLayout extends RenderLayoutBox { return true; } final AlignSelf alignSelf = _getAlignSelf(child); - return alignSelf == AlignSelf.baseline || alignSelf == AlignSelf.lastBaseline; + return alignSelf == AlignSelf.baseline || + alignSelf == AlignSelf.lastBaseline; } bool _subtreeHasPendingIntrinsicMeasureInvalidation(RenderBox root) { @@ -2722,7 +2869,8 @@ class RenderFlexLayout extends RenderLayoutBox { return true; } - if (root is ContainerRenderObjectMixin>) { + if (root is ContainerRenderObjectMixin>) { RenderBox? child = (root as dynamic).firstChild as RenderBox?; while (child != null) { if (_subtreeHasPendingIntrinsicMeasureInvalidation(child)) { @@ -2744,6 +2892,22 @@ class RenderFlexLayout extends RenderLayoutBox { } bool _hasWrappingFlexAncestor() { + if (renderBoxModelInLayoutStack.isNotEmpty) { + final int currentLayoutPassId = renderBoxModelLayoutPassId; + final bool? cachedValue = _hasWrappingFlexAncestorCached; + if (_wrappingFlexAncestorCachePassId == currentLayoutPassId && + cachedValue != null) { + return cachedValue; + } + final bool hasWrappingAncestor = _computeHasWrappingFlexAncestor(); + _wrappingFlexAncestorCachePassId = currentLayoutPassId; + _hasWrappingFlexAncestorCached = hasWrappingAncestor; + return hasWrappingAncestor; + } + return _computeHasWrappingFlexAncestor(); + } + + bool _computeHasWrappingFlexAncestor() { RenderObject? ancestor = parent; while (ancestor != null) { if (ancestor is RenderFlexLayout) { @@ -2775,7 +2939,8 @@ class RenderFlexLayout extends RenderLayoutBox { }; } - if (root is ContainerRenderObjectMixin>) { + if (root is ContainerRenderObjectMixin>) { RenderBox? child = (root as dynamic).firstChild as RenderBox?; while (child != null) { final Map? details = @@ -2805,8 +2970,8 @@ class RenderFlexLayout extends RenderLayoutBox { root.clearIntrinsicMeasurementInvalidationAfterMeasurement(); } - if (root - is ContainerRenderObjectMixin>) { + if (root is ContainerRenderObjectMixin>) { RenderBox? child = (root as dynamic).firstChild as RenderBox?; while (child != null) { _clearSubtreeIntrinsicMeasurementInvalidationAfterMeasurement(child); @@ -2846,8 +3011,7 @@ class RenderFlexLayout extends RenderLayoutBox { return (value * 100).round(); } - int _computeReusableIntrinsicMeasurementStyleSignature( - CSSRenderStyle style) { + int _computeReusableIntrinsicMeasurementStyleSignature(CSSRenderStyle style) { if (renderBoxModelInLayoutStack.isNotEmpty) { final int currentLayoutPassId = renderBoxModelLayoutPassId; if (_reusableIntrinsicStyleSignaturePassId != currentLayoutPassId) { @@ -2864,11 +3028,16 @@ class RenderFlexLayout extends RenderLayoutBox { int hash = 0; hash = _hashReusableIntrinsicMeasurementState(hash, style.display.hashCode); - hash = _hashReusableIntrinsicMeasurementState(hash, style.position.hashCode); - hash = _hashReusableIntrinsicMeasurementState(hash, style.whiteSpace.hashCode); - hash = _hashReusableIntrinsicMeasurementState(hash, style.wordBreak.hashCode); - hash = _hashReusableIntrinsicMeasurementState(hash, style.textAlign.hashCode); - hash = _hashReusableIntrinsicMeasurementState(hash, style.fontStyle.hashCode); + hash = + _hashReusableIntrinsicMeasurementState(hash, style.position.hashCode); + hash = + _hashReusableIntrinsicMeasurementState(hash, style.whiteSpace.hashCode); + hash = + _hashReusableIntrinsicMeasurementState(hash, style.wordBreak.hashCode); + hash = + _hashReusableIntrinsicMeasurementState(hash, style.textAlign.hashCode); + hash = + _hashReusableIntrinsicMeasurementState(hash, style.fontStyle.hashCode); hash = _hashReusableIntrinsicMeasurementState(hash, style.fontWeight.value); hash = _hashReusableIntrinsicMeasurementState( hash, @@ -2876,18 +3045,26 @@ class RenderFlexLayout extends RenderLayoutBox { ); hash = _hashReusableIntrinsicMeasurementState( hash, - _quantizeReusableIntrinsicMeasurementDouble(style.lineHeight.computedValue), + _quantizeReusableIntrinsicMeasurementDouble( + style.lineHeight.computedValue), ); hash = _hashReusableIntrinsicMeasurementState( hash, - _quantizeReusableIntrinsicMeasurementDouble(style.textIndent.computedValue), + _quantizeReusableIntrinsicMeasurementDouble( + style.textIndent.computedValue), ); - hash = _hashReusableIntrinsicMeasurementState(hash, style.width.type.hashCode); - hash = _hashReusableIntrinsicMeasurementState(hash, style.height.type.hashCode); - hash = _hashReusableIntrinsicMeasurementState(hash, style.minWidth.type.hashCode); - hash = _hashReusableIntrinsicMeasurementState(hash, style.maxWidth.type.hashCode); - hash = _hashReusableIntrinsicMeasurementState(hash, style.minHeight.type.hashCode); - hash = _hashReusableIntrinsicMeasurementState(hash, style.maxHeight.type.hashCode); + hash = + _hashReusableIntrinsicMeasurementState(hash, style.width.type.hashCode); + hash = _hashReusableIntrinsicMeasurementState( + hash, style.height.type.hashCode); + hash = _hashReusableIntrinsicMeasurementState( + hash, style.minWidth.type.hashCode); + hash = _hashReusableIntrinsicMeasurementState( + hash, style.maxWidth.type.hashCode); + hash = _hashReusableIntrinsicMeasurementState( + hash, style.minHeight.type.hashCode); + hash = _hashReusableIntrinsicMeasurementState( + hash, style.maxHeight.type.hashCode); if (style.width.isNotAuto) { hash = _hashReusableIntrinsicMeasurementState( hash, @@ -2903,25 +3080,29 @@ class RenderFlexLayout extends RenderLayoutBox { if (style.minWidth.isNotAuto) { hash = _hashReusableIntrinsicMeasurementState( hash, - _quantizeReusableIntrinsicMeasurementDouble(style.minWidth.computedValue), + _quantizeReusableIntrinsicMeasurementDouble( + style.minWidth.computedValue), ); } if (!style.maxWidth.isNone) { hash = _hashReusableIntrinsicMeasurementState( hash, - _quantizeReusableIntrinsicMeasurementDouble(style.maxWidth.computedValue), + _quantizeReusableIntrinsicMeasurementDouble( + style.maxWidth.computedValue), ); } if (style.minHeight.isNotAuto) { hash = _hashReusableIntrinsicMeasurementState( hash, - _quantizeReusableIntrinsicMeasurementDouble(style.minHeight.computedValue), + _quantizeReusableIntrinsicMeasurementDouble( + style.minHeight.computedValue), ); } if (!style.maxHeight.isNone) { hash = _hashReusableIntrinsicMeasurementState( hash, - _quantizeReusableIntrinsicMeasurementDouble(style.maxHeight.computedValue), + _quantizeReusableIntrinsicMeasurementDouble( + style.maxHeight.computedValue), ); } if (style.flexBasis != null) { @@ -2942,8 +3123,7 @@ class RenderFlexLayout extends RenderLayoutBox { BoxConstraints childConstraints, { RenderFlowLayout? flowChild, }) { - final RenderFlowLayout? effectiveFlowChild = - flowChild ?? + final RenderFlowLayout? effectiveFlowChild = flowChild ?? _getCacheableIntrinsicMeasureFlowChild( child, allowAnonymous: true, @@ -2954,7 +3134,8 @@ class RenderFlexLayout extends RenderLayoutBox { int hash = 0; final CSSRenderStyle style = effectiveFlowChild.renderStyle; - hash = _hashReusableIntrinsicMeasurementState(hash, effectiveFlowChild.hashCode); + hash = _hashReusableIntrinsicMeasurementState( + hash, effectiveFlowChild.hashCode); hash = _hashReusableIntrinsicMeasurementState( hash, _quantizeReusableIntrinsicMeasurementDouble(childConstraints.minWidth), @@ -2975,7 +3156,8 @@ class RenderFlexLayout extends RenderLayoutBox { hash, _computeReusableIntrinsicMeasurementStyleSignature(style), ); - final InlineFormattingContext? ifc = effectiveFlowChild.inlineFormattingContext; + final InlineFormattingContext? ifc = + effectiveFlowChild.inlineFormattingContext; if (ifc != null) { hash = _hashReusableIntrinsicMeasurementState( hash, @@ -2994,8 +3176,7 @@ class RenderFlexLayout extends RenderLayoutBox { return null; } - final RenderFlowLayout? effectiveFlowChild = - flowChild ?? + final RenderFlowLayout? effectiveFlowChild = flowChild ?? _getCacheableIntrinsicMeasureFlowChild( child, allowAnonymous: true, @@ -3004,7 +3185,8 @@ class RenderFlexLayout extends RenderLayoutBox { return null; } - final InlineFormattingContext? ifc = effectiveFlowChild.inlineFormattingContext; + final InlineFormattingContext? ifc = + effectiveFlowChild.inlineFormattingContext; if (ifc == null || !ifc.isPlainTextOnlyForSharedReuse) { return null; } @@ -3035,11 +3217,9 @@ class RenderFlexLayout extends RenderLayoutBox { _FlexIntrinsicMeasurementLookupResult _lookupReusableIntrinsicMeasurement( RenderBox child, - BoxConstraints childConstraints, - { + BoxConstraints childConstraints, { bool allowAnonymous = false, - } - ) { + }) { if (!allowAnonymous) { return const _FlexIntrinsicMeasurementLookupResult(); } @@ -3073,6 +3253,7 @@ class RenderFlexLayout extends RenderLayoutBox { } return sharedPlainTextSignature; } + final _FlexIntrinsicMeasurementCacheBucket? cacheBucket = _childrenIntrinsicMeasureCache[child]; if (cacheBucket == null || cacheBucket.entries.isEmpty) { @@ -3080,8 +3261,7 @@ class RenderFlexLayout extends RenderLayoutBox { getSharedPlainTextSignature() == null ? null : _sharedPlainTextFlexIntrinsicMeasurementCache[ - sharedPlainTextSignature - ]; + sharedPlainTextSignature]; if (sharedEntry != null) { final _FlexIntrinsicMeasurementCacheBucket bucket = _childrenIntrinsicMeasureCache[child] ?? @@ -3107,8 +3287,7 @@ class RenderFlexLayout extends RenderLayoutBox { getSharedPlainTextSignature() == null ? null : _sharedPlainTextFlexIntrinsicMeasurementCache[ - sharedPlainTextSignature - ]; + sharedPlainTextSignature]; if (sharedEntry != null) { cacheBucket.store(sharedEntry); return _FlexIntrinsicMeasurementLookupResult( @@ -3184,8 +3363,7 @@ class RenderFlexLayout extends RenderLayoutBox { RenderBox child, BoxConstraints childConstraints, Size childSize, - double intrinsicMainSize, - { + double intrinsicMainSize, { RenderFlowLayout? flowChild, int? reusableStateSignature, int? sharedPlainTextSignature, @@ -3200,7 +3378,7 @@ class RenderFlexLayout extends RenderLayoutBox { } final _FlexIntrinsicMeasurementCacheBucket bucket = _childrenIntrinsicMeasureCache[child] ?? - _FlexIntrinsicMeasurementCacheBucket(); + _FlexIntrinsicMeasurementCacheBucket(); reusableStateSignature ??= _computeReusableIntrinsicMeasurementStateSignature( child, @@ -3231,8 +3409,8 @@ class RenderFlexLayout extends RenderLayoutBox { _sharedPlainTextFlexIntrinsicMeasurementCache.keys.first, ); } - _sharedPlainTextFlexIntrinsicMeasurementCache[ - sharedPlainTextSignature] = entry; + _sharedPlainTextFlexIntrinsicMeasurementCache[sharedPlainTextSignature] = + entry; } } @@ -3244,7 +3422,8 @@ class RenderFlexLayout extends RenderLayoutBox { if (flowChild == null) { return false; } - return child is RenderEventListener || flowChild.renderStyle.isSelfAnonymousFlowLayout(); + return child is RenderEventListener || + flowChild.renderStyle.isSelfAnonymousFlowLayout(); } bool _shouldAvoidParentUsesSizeForFlexChild(RenderBox child) { @@ -3430,8 +3609,8 @@ class RenderFlexLayout extends RenderLayoutBox { } } - if (effectiveRoot - is ContainerRenderObjectMixin>) { + if (effectiveRoot is ContainerRenderObjectMixin>) { RenderBox? child = (effectiveRoot as dynamic).firstChild as RenderBox?; while (child != null) { if (_flowSubtreeContainsReusableTextHeavyContent(child)) { @@ -3558,8 +3737,7 @@ class RenderFlexLayout extends RenderLayoutBox { final bool hasFlexibleLengths = metrics.totalFlexGrow > 0 || metrics.totalFlexShrink > 0; for (final _RunChild runChild in metrics.runChildren) { - if (hasFlexibleLengths && - !_hasEffectivelyTightMainAxisSize(runChild)) { + if (hasFlexibleLengths && !_hasEffectivelyTightMainAxisSize(runChild)) { _recordEarlyFastPathReject( _FlexFastPathRejectReason.childNonTightWidth, child: runChild.child, @@ -3643,8 +3821,7 @@ class RenderFlexLayout extends RenderLayoutBox { return false; } - if (hasFlexibleLengths && - !_hasEffectivelyTightMainAxisSize(runChild)) { + if (hasFlexibleLengths && !_hasEffectivelyTightMainAxisSize(runChild)) { return false; } } @@ -3653,11 +3830,14 @@ class RenderFlexLayout extends RenderLayoutBox { return true; } - void _storePercentageConstraintChildrenOldConstraints(List children) { + void _storePercentageConstraintChildrenOldConstraints( + List children) { for (final RenderBox child in children) { final RenderBoxModel? box = child is RenderBoxModel ? child - : (child is RenderEventListener ? child.child as RenderBoxModel? : null); + : (child is RenderEventListener + ? child.child as RenderBoxModel? + : null); if (box == null) { continue; } @@ -3674,7 +3854,8 @@ class RenderFlexLayout extends RenderLayoutBox { } } - List<_RunMetrics>? _tryBuildEarlyNoFlexNoStretchNoBaselineRunMetrics(List children) { + List<_RunMetrics>? _tryBuildEarlyNoFlexNoStretchNoBaselineRunMetrics( + List children) { if (!_isHorizontalFlexDirection) { _recordEarlyFastPathReject( _FlexFastPathRejectReason.verticalDirection, @@ -3699,9 +3880,11 @@ class RenderFlexLayout extends RenderLayoutBox { for (int childIndex = 0; childIndex < children.length; childIndex++) { final RenderBox child = children[childIndex]; + final RenderBoxModel? effectiveChild = + child is RenderBoxModel ? child : null; final BoxConstraints childConstraints; - if (child is RenderBoxModel) { - childConstraints = child.getConstraints(); + if (effectiveChild != null) { + childConstraints = effectiveChild.getConstraints(); } else if (child is RenderConstrainedBox) { childConstraints = child.additionalConstraints; } else { @@ -3721,8 +3904,6 @@ class RenderFlexLayout extends RenderLayoutBox { return null; } - final RenderBoxModel? effectiveChild = - child is RenderBoxModel ? child : null; if (!_canReuseCurrentFlexMeasurement( child, childConstraints, @@ -3732,10 +3913,15 @@ class RenderFlexLayout extends RenderLayoutBox { } _cacheOriginalConstraintsIfNeeded(child, childConstraints); - final RenderLayoutParentData? childParentData = child.parentData as RenderLayoutParentData?; + final RenderLayoutParentData? childParentData = + child.parentData as RenderLayoutParentData?; childParentData?.runIndex = 0; - final double childMainSize = _getMainSize(child); + final Size childSize = _getChildSize(child)!; + final double childMainSize = + _isHorizontalFlexDirection ? childSize.width : childSize.height; + final double childCrossSize = + _isHorizontalFlexDirection ? childSize.height : childSize.width; _childrenIntrinsicMainSizes[child] = childMainSize; final _RunChild runChild = _createRunChildMetadata( @@ -3747,9 +3933,9 @@ class RenderFlexLayout extends RenderLayoutBox { if (runChildren.isNotEmpty) { runMainAxisExtent += mainAxisGap; } - runMainAxisExtent += _getRunChildMainAxisExtent(runChild); - runCrossAxisExtent = - math.max(runCrossAxisExtent, _getRunChildCrossAxisExtent(runChild)); + runMainAxisExtent += childMainSize + runChild.mainAxisExtentAdjustment; + runCrossAxisExtent = math.max(runCrossAxisExtent, + childCrossSize + runChild.crossAxisExtentAdjustment); runChildren.add(runChild); if (runChild.flexGrow > 0) { @@ -3761,7 +3947,8 @@ class RenderFlexLayout extends RenderLayoutBox { } final List<_RunMetrics> runMetrics = <_RunMetrics>[ - _RunMetrics(runMainAxisExtent, runCrossAxisExtent, totalFlexGrow, totalFlexShrink, 0, runChildren, 0) + _RunMetrics(runMainAxisExtent, runCrossAxisExtent, totalFlexGrow, + totalFlexShrink, 0, runChildren, 0) ]; _flexLineBoxMetrics = runMetrics; @@ -3807,11 +3994,14 @@ class RenderFlexLayout extends RenderLayoutBox { // Prepare children of different type for layout. RenderBox? child = firstChild; while (child != null) { - final RenderLayoutParentData childParentData = child.parentData as RenderLayoutParentData; + final RenderLayoutParentData childParentData = + child.parentData as RenderLayoutParentData; if (child is RenderBoxModel && - (child.renderStyle.isSelfPositioned() || child.renderStyle.isSelfStickyPosition())) { + (child.renderStyle.isSelfPositioned() || + child.renderStyle.isSelfStickyPosition())) { positionedChildren.add(child); - } else if (child is RenderPositionPlaceholder && _isPlaceholderPositioned(child)) { + } else if (child is RenderPositionPlaceholder && + _isPlaceholderPositioned(child)) { positionPlaceholderChildren.add(child); } else { flexItemChildren.add(child); @@ -3856,7 +4046,8 @@ class RenderFlexLayout extends RenderLayoutBox { for (RenderBoxModel child in positionedChildren) { final CSSRenderStyle rs = child.renderStyle; final CSSPositionType pos = rs.position; - final bool isAbsOrFixed = pos == CSSPositionType.absolute || pos == CSSPositionType.fixed; + final bool isAbsOrFixed = + pos == CSSPositionType.absolute || pos == CSSPositionType.fixed; final bool hasExplicitMaxHeight = !rs.maxHeight.isNone; final bool hasExplicitMinHeight = !rs.minHeight.isAuto; if (isAbsOrFixed && @@ -3866,7 +4057,8 @@ class RenderFlexLayout extends RenderLayoutBox { rs.bottom.isNotAuto && !hasExplicitMaxHeight && !hasExplicitMinHeight) { - CSSPositionedLayout.layoutPositionedChild(this, child, needsRelayout: true); + CSSPositionedLayout.layoutPositionedChild(this, child, + needsRelayout: true); } } @@ -3875,7 +4067,8 @@ class RenderFlexLayout extends RenderLayoutBox { } // init overflowLayout size - initOverflowLayout(Rect.fromLTRB(0, 0, size.width, size.height), Rect.fromLTRB(0, 0, size.width, size.height)); + initOverflowLayout(Rect.fromLTRB(0, 0, size.width, size.height), + Rect.fromLTRB(0, 0, size.width, size.height)); // calculate all flexItem child overflow size addOverflowLayoutFromChildren(orderedChildren); @@ -3936,7 +4129,8 @@ class RenderFlexLayout extends RenderLayoutBox { runMetrics = _tryBuildEarlyNoFlexNoStretchNoBaselineRunMetrics(children); } if (runMetrics != null) { - final bool hasStretchedChildren = _hasStretchedChildrenInCrossAxis(runMetrics); + final bool hasStretchedChildren = + _hasStretchedChildrenInCrossAxis(runMetrics); if (!hasStretchedChildren && _canAttemptFullEarlyFastPath(runMetrics)) { final _FlexResolutionInputs inputs = _computeFlexResolutionInputs(); if (_tryNoFlexNoStretchNoBaselineFastPath( @@ -3971,7 +4165,8 @@ class RenderFlexLayout extends RenderLayoutBox { if (runMetrics == null) { // Layout children to compute metrics of flex lines. if (_enableFlexProfileSections) { - developer.Timeline.startSync('RenderFlex.layoutFlexItems.computeRunMetrics', + developer.Timeline.startSync( + 'RenderFlex.layoutFlexItems.computeRunMetrics', arguments: {'renderObject': describeIdentity(this)}); } @@ -3985,7 +4180,8 @@ class RenderFlexLayout extends RenderLayoutBox { _setContainerSize(runMetrics); if (_enableFlexProfileSections) { - developer.Timeline.startSync('RenderFlex.layoutFlexItems.adjustChildrenSize'); + developer.Timeline.startSync( + 'RenderFlex.layoutFlexItems.adjustChildrenSize'); } // Adjust children size based on flex properties which may affect children size. @@ -3999,7 +4195,8 @@ class RenderFlexLayout extends RenderLayoutBox { } if (_enableFlexProfileSections) { - developer.Timeline.startSync('RenderFlex.layoutFlexItems.setChildrenOffset'); + developer.Timeline.startSync( + 'RenderFlex.layoutFlexItems.setChildrenOffset'); } // Set children offset based on flex alignment properties. @@ -4010,7 +4207,8 @@ class RenderFlexLayout extends RenderLayoutBox { } if (_enableFlexProfileSections) { - developer.Timeline.startSync('RenderFlex.layoutFlexItems.setMaxScrollableSize'); + developer.Timeline.startSync( + 'RenderFlex.layoutFlexItems.setMaxScrollableSize'); } // Set the size of scrollable overflow area for flex layout. @@ -4029,27 +4227,32 @@ class RenderFlexLayout extends RenderLayoutBox { // Cache CSS baselines for this flex container during layout to avoid cross-child baseline computation later. double? containerBaseline; CSSDisplay? effectiveDisplay = renderStyle.effectiveDisplay; - bool isDisplayInline = effectiveDisplay != CSSDisplay.block && effectiveDisplay != CSSDisplay.flex; + bool isDisplayInline = effectiveDisplay != CSSDisplay.block && + effectiveDisplay != CSSDisplay.flex; double? getChildBaselineDistance(RenderBox child) { // Prefer WebF's cached CSS baselines, which are safe to access even when the // render tree is not attached to a PipelineOwner (e.g. offscreen/manual layout). if (child is RenderBoxModel) { - final double? css = child.computeCssFirstBaselineOf(TextBaseline.alphabetic); + final double? css = + child.computeCssFirstBaselineOf(TextBaseline.alphabetic); if (css != null) return css; } else if (child is RenderPositionPlaceholder) { final RenderBoxModel? positioned = child.positioned; - final double? css = positioned?.computeCssFirstBaselineOf(TextBaseline.alphabetic); + final double? css = + positioned?.computeCssFirstBaselineOf(TextBaseline.alphabetic); if (css != null) return css; } // Avoid RenderBox.getDistanceToBaseline when detached; it asserts on owner!. if (!child.attached) { if (child is RenderBoxModel) { - return child.boxSize?.height ?? (child.hasSize ? child.size.height : null); + return child.boxSize?.height ?? + (child.hasSize ? child.size.height : null); } if (child is RenderPositionPlaceholder) { - return child.boxSize?.height ?? (child.hasSize ? child.size.height : null); + return child.boxSize?.height ?? + (child.hasSize ? child.size.height : null); } return child.hasSize ? child.size.height : null; } @@ -4087,9 +4290,11 @@ class RenderFlexLayout extends RenderLayoutBox { // baseline-aligned item no longer anchors the container baseline in IFC. if (_isChildCrossAxisMarginAutoExist(candidate)) return false; final AlignSelf self = _getAlignSelf(candidate); - if (self == AlignSelf.baseline || self == AlignSelf.lastBaseline) return true; + if (self == AlignSelf.baseline || self == AlignSelf.lastBaseline) + return true; if (self == AlignSelf.auto && - (renderStyle.alignItems == AlignItems.baseline || renderStyle.alignItems == AlignItems.lastBaseline)) { + (renderStyle.alignItems == AlignItems.baseline || + renderStyle.alignItems == AlignItems.lastBaseline)) { return true; } return false; @@ -4133,7 +4338,8 @@ class RenderFlexLayout extends RenderLayoutBox { // For inline flex containers, include bottom margin to synthesize an // external baseline from the bottom margin edge. if (isDisplayInline) { - containerBaseline = borderBoxHeight + renderStyle.marginBottom.computedValue; + containerBaseline = + borderBoxHeight + renderStyle.marginBottom.computedValue; } else { containerBaseline = borderBoxHeight; } @@ -4151,15 +4357,18 @@ class RenderFlexLayout extends RenderLayoutBox { final List<_RunChild> firstRunChildren = firstLineMetrics.runChildren; if (firstRunChildren.isNotEmpty) { RenderBox? baselineChild; - double? baselineDistance; // distance from child's border-top to its baseline + double? + baselineDistance; // distance from child's border-top to its baseline RenderBox? fallbackChild; double? fallbackBaseline; bool participatesInBaseline(RenderBox candidate) { final AlignSelf self = _getAlignSelf(candidate); - if (self == AlignSelf.baseline || self == AlignSelf.lastBaseline) return true; + if (self == AlignSelf.baseline || self == AlignSelf.lastBaseline) + return true; if (self == AlignSelf.auto && - (renderStyle.alignItems == AlignItems.baseline || renderStyle.alignItems == AlignItems.lastBaseline)) { + (renderStyle.alignItems == AlignItems.baseline || + renderStyle.alignItems == AlignItems.lastBaseline)) { return true; } return false; @@ -4193,7 +4402,8 @@ class RenderFlexLayout extends RenderLayoutBox { (self == AlignSelf.baseline || self == AlignSelf.lastBaseline || (self == AlignSelf.auto && - (renderStyle.alignItems == AlignItems.baseline || renderStyle.alignItems == AlignItems.lastBaseline))); + (renderStyle.alignItems == AlignItems.baseline || + renderStyle.alignItems == AlignItems.lastBaseline))); double dy = 0; if (child.parentData is RenderLayoutParentData) { dy = (child.parentData as RenderLayoutParentData).offset.dy; @@ -4218,7 +4428,8 @@ class RenderFlexLayout extends RenderLayoutBox { // Prefer the first baseline-participating child (excluding cross-axis auto margins); // otherwise fall back to the first child that exposes a baseline; otherwise the first item. final RenderBox? chosen = baselineChild ?? fallbackChild; - final double? chosenBaseline = baselineChild != null ? baselineDistance : fallbackBaseline; + final double? chosenBaseline = + baselineChild != null ? baselineDistance : fallbackBaseline; if (chosen != null) { if (chosenBaseline != null) { @@ -4230,7 +4441,8 @@ class RenderFlexLayout extends RenderLayoutBox { dy = pd.offset.dy; } if (chosen is RenderBoxModel) { - final Offset? rel = CSSPositionedLayout.getRelativeOffset(chosen.renderStyle); + final Offset? rel = + CSSPositionedLayout.getRelativeOffset(chosen.renderStyle); if (rel != null) dy -= rel.dy; } containerBaseline = chosenBaseline + dy; @@ -4239,7 +4451,8 @@ class RenderFlexLayout extends RenderLayoutBox { final double borderBoxHeight = boxSize?.height ?? size.height; // If inline-level (inline-flex), synthesize from bottom margin edge. if (isDisplayInline) { - containerBaseline = borderBoxHeight + renderStyle.marginBottom.computedValue; + containerBaseline = + borderBoxHeight + renderStyle.marginBottom.computedValue; } else { containerBaseline = borderBoxHeight; } @@ -4255,7 +4468,8 @@ class RenderFlexLayout extends RenderLayoutBox { List positionPlaceholderChildren = [child]; // Layout children to compute metrics of flex lines. - List<_RunMetrics> runMetrics = _computeRunMetrics(positionPlaceholderChildren); + List<_RunMetrics> runMetrics = + _computeRunMetrics(positionPlaceholderChildren); // Set children offset based on flex alignment properties. _setChildrenOffset(runMetrics); @@ -4263,12 +4477,15 @@ class RenderFlexLayout extends RenderLayoutBox { // Layout children in normal flow order to calculate metrics of flex lines according to its constraints // and flex-wrap property. - List<_RunMetrics> _computeRunMetrics(List children,) { + List<_RunMetrics> _computeRunMetrics( + List children, + ) { List<_RunMetrics> runMetrics = <_RunMetrics>[]; if (children.isEmpty) return runMetrics; final bool isHorizontal = _isHorizontalFlexDirection; - final bool isWrap = renderStyle.flexWrap == FlexWrap.wrap || renderStyle.flexWrap == FlexWrap.wrapReverse; + final bool isWrap = renderStyle.flexWrap == FlexWrap.wrap || + renderStyle.flexWrap == FlexWrap.wrapReverse; final double mainAxisGap = _getMainAxisGap(); double runMainAxisExtent = 0.0; @@ -4309,9 +4526,8 @@ class RenderFlexLayout extends RenderLayoutBox { final BoxConstraints childConstraints = _getIntrinsicConstraints(child); final RenderBoxModel? effectiveChild = child is RenderBoxModel ? child : null; - final bool isMetricsOnlyMeasureChild = - allowAnonymousMetricsOnlyCache && - _isMetricsOnlyIntrinsicMeasureChild(child); + final bool isMetricsOnlyMeasureChild = allowAnonymousMetricsOnlyCache && + _isMetricsOnlyIntrinsicMeasureChild(child); final _FlexIntrinsicMeasurementLookupResult cacheLookup = _lookupReusableIntrinsicMeasurement( child, @@ -4382,24 +4598,27 @@ class RenderFlexLayout extends RenderLayoutBox { double? candidate; if (flowChild.inlineFormattingContext != null) { // Paragraph max-intrinsic width approximates the max-content contribution. - final double paraMax = flowChild.inlineFormattingContext!.paragraphMaxIntrinsicWidth; + final double paraMax = flowChild + .inlineFormattingContext!.paragraphMaxIntrinsicWidth; // Convert content-width to border-box width by adding horizontal padding + borders. - final double paddingBorderH = - cs.paddingLeft.computedValue + - cs.paddingRight.computedValue + - cs.effectiveBorderLeftWidth.computedValue + - cs.effectiveBorderRightWidth.computedValue; + final double paddingBorderH = cs.paddingLeft.computedValue + + cs.paddingRight.computedValue + + cs.effectiveBorderLeftWidth.computedValue + + cs.effectiveBorderRightWidth.computedValue; candidate = (paraMax.isFinite ? paraMax : 0) + paddingBorderH; } else { // Fallback: use max intrinsic width (already includes padding/border). - final double maxIntrinsic = flowChild.getMaxIntrinsicWidth(double.infinity); + final double maxIntrinsic = + flowChild.getMaxIntrinsicWidth(double.infinity); if (maxIntrinsic.isFinite) { candidate = maxIntrinsic; } } // If the currently measured intrinsic width is larger (e.g., filled to container), // prefer the content-based candidate to avoid unintended expansion. - if (candidate != null && candidate > 0 && candidate < intrinsicMain) { + if (candidate != null && + candidate > 0 && + candidate < intrinsicMain) { intrinsicMain = candidate; } } @@ -4423,10 +4642,14 @@ class RenderFlexLayout extends RenderLayoutBox { // intrinsicMain is the border-box main size. In WebF (border-box model), // min-width/max-width are already specified for the border box. Do not // add padding/border again when clamping. - if (maxMain != null && maxMain.isFinite && intrinsicMain > maxMain) { + if (maxMain != null && + maxMain.isFinite && + intrinsicMain > maxMain) { intrinsicMain = maxMain; } - if (minMain != null && minMain.isFinite && intrinsicMain < minMain) { + if (minMain != null && + minMain.isFinite && + intrinsicMain < minMain) { intrinsicMain = minMain; } } @@ -4437,18 +4660,21 @@ class RenderFlexLayout extends RenderLayoutBox { bool hasPctMaxMain = isHorizontal ? child.renderStyle.maxWidth.type == CSSLengthType.PERCENTAGE : child.renderStyle.maxHeight.type == CSSLengthType.PERCENTAGE; - bool hasAutoMain = isHorizontal ? child.renderStyle.width.isAuto : child.renderStyle.height - .isAuto; + bool hasAutoMain = isHorizontal + ? child.renderStyle.width.isAuto + : child.renderStyle.height.isAuto; if (hasPctMaxMain && hasAutoMain) { double paddingBorderMain = isHorizontal ? (child.renderStyle.effectiveBorderLeftWidth.computedValue + - child.renderStyle.effectiveBorderRightWidth.computedValue + - child.renderStyle.paddingLeft.computedValue + - child.renderStyle.paddingRight.computedValue) + child + .renderStyle.effectiveBorderRightWidth.computedValue + + child.renderStyle.paddingLeft.computedValue + + child.renderStyle.paddingRight.computedValue) : (child.renderStyle.effectiveBorderTopWidth.computedValue + - child.renderStyle.effectiveBorderBottomWidth.computedValue + - child.renderStyle.paddingTop.computedValue + - child.renderStyle.paddingBottom.computedValue); + child.renderStyle.effectiveBorderBottomWidth + .computedValue + + child.renderStyle.paddingTop.computedValue + + child.renderStyle.paddingBottom.computedValue); // Check if this is an empty element (no content) using DOM-based detection bool isEmptyElement = false; @@ -4482,11 +4708,11 @@ class RenderFlexLayout extends RenderLayoutBox { ); } - final RenderLayoutParentData? childParentData = child.parentData as RenderLayoutParentData?; + final RenderLayoutParentData? childParentData = + child.parentData as RenderLayoutParentData?; _childrenIntrinsicMainSizes[child] = intrinsicMain; - Size? intrinsicChildSize = _getChildSize(child, shouldUseIntrinsicMainSize: true); final double? usedFlexBasis = effectiveChild != null ? _getUsedFlexBasis(child) : null; final _RunChild runChild = _createRunChildMetadata( @@ -4496,18 +4722,19 @@ class RenderFlexLayout extends RenderLayoutBox { usedFlexBasis: usedFlexBasis, ); - double childMainAxisExtent = _getRunChildMainAxisExtent( - runChild, - shouldUseIntrinsicMainSize: true, - ); - double childCrossAxisExtent = _getRunChildCrossAxisExtent(runChild); + final double childCrossSize = + isHorizontal ? childSize.height : childSize.width; + double childMainAxisExtent = + intrinsicMain + runChild.mainAxisExtentAdjustment; + double childCrossAxisExtent = + childCrossSize + runChild.crossAxisExtentAdjustment; // Include gap spacing in flex line limit check double gapSpacing = runChildren.isNotEmpty ? mainAxisGap : 0; - bool isExceedFlexLineLimit = runMainAxisExtent + gapSpacing + childMainAxisExtent > flexLineLimit; + bool isExceedFlexLineLimit = + runMainAxisExtent + gapSpacing + childMainAxisExtent > + flexLineLimit; // calculate flex line - if (isWrap && - runChildren.isNotEmpty && - isExceedFlexLineLimit) { + if (isWrap && runChildren.isNotEmpty && isExceedFlexLineLimit) { runMetrics.add(_RunMetrics( runMainAxisExtent, runCrossAxisExtent, @@ -4535,8 +4762,7 @@ class RenderFlexLayout extends RenderLayoutBox { // Vertical align is only valid for inline box. // Baseline alignment in column direction behave the same as flex-start. AlignSelf alignSelf = runChild.alignSelf; - bool isBaselineAlign = - alignSelf == AlignSelf.baseline || + bool isBaselineAlign = alignSelf == AlignSelf.baseline || alignSelf == AlignSelf.lastBaseline || renderStyle.alignItems == AlignItems.baseline || renderStyle.alignItems == AlignItems.lastBaseline; @@ -4550,10 +4776,9 @@ class RenderFlexLayout extends RenderLayoutBox { childMarginBottom = child.renderStyle.marginBottom.computedValue; } if (DebugFlags.debugLogFlexBaselineEnabled) { - final Size? ic = intrinsicChildSize; renderingLogger.finer('[FlexBaseline] PASS2 child=' '${child.runtimeType}#${child.hashCode} ' - 'intrinsicSize=${ic?.width.toStringAsFixed(2)}x${ic?.height.toStringAsFixed(2)} ' + 'intrinsicSize=${childSize.width.toStringAsFixed(2)}x${childSize.height.toStringAsFixed(2)} ' 'ascent=${childAscent.toStringAsFixed(2)} ' 'mT=${childMarginTop.toStringAsFixed(2)} mB=${childMarginBottom.toStringAsFixed(2)}'); } @@ -4562,7 +4787,7 @@ class RenderFlexLayout extends RenderLayoutBox { maxSizeAboveBaseline, ); maxSizeBelowBaseline = math.max( - childMarginTop + childMarginBottom + intrinsicChildSize!.height - childAscent, + childMarginTop + childMarginBottom + childSize.height - childAscent, maxSizeBelowBaseline, ); runCrossAxisExtent = maxSizeAboveBaseline + maxSizeBelowBaseline; @@ -4573,7 +4798,8 @@ class RenderFlexLayout extends RenderLayoutBox { 'runCross=${runCrossAxisExtent.toStringAsFixed(2)}'); } } else { - runCrossAxisExtent = math.max(runCrossAxisExtent, childCrossAxisExtent); + runCrossAxisExtent = + math.max(runCrossAxisExtent, childCrossAxisExtent); } // Per CSS Flexbox §9.7, keep two sizes: @@ -4590,7 +4816,9 @@ class RenderFlexLayout extends RenderLayoutBox { // items share free space evenly regardless of padding/border, while the // non-flex portion (padding/border) is accounted for separately in totalSpace. final CSSLengthValue? fb = effectiveChild?.renderStyle.flexBasis; - if (fb != null && fb.type == CSSLengthType.PERCENTAGE && fb.computedValue == 0) { + if (fb != null && + fb.type == CSSLengthType.PERCENTAGE && + fb.computedValue == 0) { baseMainSize = 0; } else { baseMainSize = usedFlexBasis; @@ -4646,7 +4874,9 @@ class RenderFlexLayout extends RenderLayoutBox { } // Compute the leading and between spacing of each flex line. - _RunSpacing _computeRunSpacing(List<_RunMetrics> runMetrics,) { + _RunSpacing _computeRunSpacing( + List<_RunMetrics> runMetrics, + ) { double? contentBoxLogicalWidth = renderStyle.contentBoxLogicalWidth; double? contentBoxLogicalHeight = renderStyle.contentBoxLogicalHeight; double containerCrossAxisExtent = 0.0; @@ -4666,7 +4896,8 @@ class RenderFlexLayout extends RenderLayoutBox { double runBetweenSpace = 0.0; // Align-content only works in when flex-wrap is no nowrap. - if (renderStyle.flexWrap == FlexWrap.wrap || renderStyle.flexWrap == FlexWrap.wrapReverse) { + if (renderStyle.flexWrap == FlexWrap.wrap || + renderStyle.flexWrap == FlexWrap.wrapReverse) { switch (renderStyle.alignContent) { case AlignContent.flexStart: case AlignContent.start: @@ -4682,7 +4913,8 @@ class RenderFlexLayout extends RenderLayoutBox { if (crossAxisFreeSpace < 0) { runBetweenSpace = 0; } else { - runBetweenSpace = runCount > 1 ? crossAxisFreeSpace / (runCount - 1) : 0.0; + runBetweenSpace = + runCount > 1 ? crossAxisFreeSpace / (runCount - 1) : 0.0; } break; case AlignContent.spaceAround: @@ -4724,7 +4956,9 @@ class RenderFlexLayout extends RenderLayoutBox { // Find the size in the cross axis of flex lines. // @TODO: add cache to avoid recalculate in one layout stage. - double _getRunsCrossSize(List<_RunMetrics> runMetrics,) { + double _getRunsCrossSize( + List<_RunMetrics> runMetrics, + ) { double crossSize = 0; double crossAxisGap = _getCrossAxisGap(); for (int i = 0; i < runMetrics.length; i++) { @@ -4739,9 +4973,12 @@ class RenderFlexLayout extends RenderLayoutBox { // Find the max size in the main axis of flex lines. // @TODO: add cache to avoid recalculate in one layout stage. - double _getRunsMaxMainSize(List<_RunMetrics> runMetrics,) { + double _getRunsMaxMainSize( + List<_RunMetrics> runMetrics, + ) { // Find the max size of flex lines. - _RunMetrics maxMainSizeMetrics = runMetrics.reduce((_RunMetrics curr, _RunMetrics next) { + _RunMetrics maxMainSizeMetrics = + runMetrics.reduce((_RunMetrics curr, _RunMetrics next) { return curr.mainAxisExtent > next.mainAxisExtent ? curr : next; }); return maxMainSizeMetrics.mainAxisExtent; @@ -4749,7 +4986,11 @@ class RenderFlexLayout extends RenderLayoutBox { // Resolve flex item length if flex-grow or flex-shrink exists. // https://www.w3.org/TR/css-flexbox-1/#resolve-flexible-lengths - bool _resolveFlexibleLengths(_RunMetrics runMetric, _FlexFactorTotals totalFlexFactor, double initialFreeSpace,) { + bool _resolveFlexibleLengths( + _RunMetrics runMetric, + _FlexFactorTotals totalFlexFactor, + double initialFreeSpace, + ) { final List<_RunChild> runChildren = runMetric.runChildren; final double totalFlexGrow = totalFlexFactor.flexGrow; final double totalFlexShrink = totalFlexFactor.flexShrink; @@ -4758,7 +4999,8 @@ class RenderFlexLayout extends RenderLayoutBox { bool isFlexGrow = initialFreeSpace > 0 && totalFlexGrow > 0; bool isFlexShrink = initialFreeSpace < 0 && totalFlexShrink > 0; - double sumFlexFactors = isFlexGrow ? totalFlexGrow : (isFlexShrink ? totalFlexShrink : 0); + double sumFlexFactors = + isFlexGrow ? totalFlexGrow : (isFlexShrink ? totalFlexShrink : 0); // Per CSS Flexbox §9.7, if the sum of the unfrozen flex items’ flex // factors is less than 1, multiply the free space by this sum. @@ -4791,7 +5033,8 @@ class RenderFlexLayout extends RenderLayoutBox { final double childFlexShrink = runChild.flexShrink; if (childFlexShrink == 0) continue; - final double baseSize = (runChild.usedFlexBasis ?? runChild.originalMainSize); + final double baseSize = + (runChild.usedFlexBasis ?? runChild.originalMainSize); totalWeightedFlexShrink += baseSize * childFlexShrink; } } @@ -4801,7 +5044,8 @@ class RenderFlexLayout extends RenderLayoutBox { if (runChild.frozen) continue; // Use used flex-basis (border-box) for originalMainSize when definite. - final double originalMainSize = runChild.usedFlexBasis ?? runChild.originalMainSize; + final double originalMainSize = + runChild.usedFlexBasis ?? runChild.originalMainSize; double computedSize = originalMainSize; double flexedMainSize = originalMainSize; @@ -4811,7 +5055,8 @@ class RenderFlexLayout extends RenderLayoutBox { // Re-evaluate grow/shrink based on current remaining free space sign. final bool doGrow = spacePerFlex != null && flexGrow > 0; - final bool doShrink = remainingFreeSpace < 0 && totalFlexShrink > 0 && flexShrink > 0; + final bool doShrink = + remainingFreeSpace < 0 && totalFlexShrink > 0 && flexShrink > 0; if (doGrow) { computedSize = originalMainSize + spacePerFlex * flexGrow; @@ -4822,7 +5067,8 @@ class RenderFlexLayout extends RenderLayoutBox { // distribution itself. if (totalWeightedFlexShrink != 0) { final double scaledShrink = originalMainSize * flexShrink; - computedSize = originalMainSize + (scaledShrink / totalWeightedFlexShrink) * remainingFreeSpace; + computedSize = originalMainSize + + (scaledShrink / totalWeightedFlexShrink) * remainingFreeSpace; } } @@ -4838,10 +5084,8 @@ class RenderFlexLayout extends RenderLayoutBox { { final RenderBoxModel? clampTarget = runChild.effectiveChild; if (clampTarget != null) { - final double minMainAxisSize = - _getRunChildMinMainAxisSize(runChild); - final double maxMainAxisSize = - _getRunChildMaxMainAxisSize(runChild); + final double minMainAxisSize = _getRunChildMinMainAxisSize(runChild); + final double maxMainAxisSize = _getRunChildMaxMainAxisSize(runChild); if (computedSize < minMainAxisSize && (computedSize - minMainAxisSize).abs() >= minFlexPrecision) { flexedMainSize = minMainAxisSize; @@ -4852,7 +5096,10 @@ class RenderFlexLayout extends RenderLayoutBox { } } - double violation = (flexedMainSize - computedSize).abs() >= minFlexPrecision ? flexedMainSize - computedSize : 0; + double violation = + (flexedMainSize - computedSize).abs() >= minFlexPrecision + ? flexedMainSize - computedSize + : 0; // Collect all the flex items with violations. if (violation > 0) { @@ -4871,7 +5118,8 @@ class RenderFlexLayout extends RenderLayoutBox { runChild.frozen = true; } } else { - List<_RunChild> violations = totalViolation < 0 ? maxViolations : minViolations; + List<_RunChild> violations = + totalViolation < 0 ? maxViolations : minViolations; // Find all the violations, set main size and freeze all the flex items. for (int i = 0; i < violations.length; i++) { @@ -4881,7 +5129,8 @@ class RenderFlexLayout extends RenderLayoutBox { // item in this iteration (relative to its original size). If the // item was clamped to its min/max, flexedMainSize equals the clamp // result; its delta reflects how much free space it actually took. - runMetric.remainingFreeSpace -= runChild.flexedMainSize - runChild.originalMainSize; + runMetric.remainingFreeSpace -= + runChild.flexedMainSize - runChild.originalMainSize; double flexGrow = runChild.flexGrow; double flexShrink = runChild.flexShrink; @@ -4936,10 +5185,13 @@ class RenderFlexLayout extends RenderLayoutBox { final bool isHorizontal = _isHorizontalFlexDirection; final double mainAxisGap = _getMainAxisGap(); final double containerStyleMin = isHorizontal - ? (renderStyle.minWidth.isNotAuto ? renderStyle.minWidth.computedValue : 0.0) - : (renderStyle.minHeight.isNotAuto ? renderStyle.minHeight.computedValue : 0.0); - final bool adjustProfilerEnabled = - _FlexAdjustFastPathProfiler.enabled; + ? (renderStyle.minWidth.isNotAuto + ? renderStyle.minWidth.computedValue + : 0.0) + : (renderStyle.minHeight.isNotAuto + ? renderStyle.minHeight.computedValue + : 0.0); + final bool adjustProfilerEnabled = _FlexAdjustFastPathProfiler.enabled; final String? adjustProfilerPath = adjustProfilerEnabled ? _describeFastPathContainer() : null; int relayoutRowCount = 0; @@ -4951,7 +5203,8 @@ class RenderFlexLayout extends RenderLayoutBox { double totalSpace = 0; for (final _RunChild runChild in runChildrenList) { - final double childSpace = runChild.usedFlexBasis ?? runChild.originalMainSize; + final double childSpace = + runChild.usedFlexBasis ?? runChild.originalMainSize; totalSpace += childSpace + runChild.mainAxisMargin; } @@ -4960,7 +5213,8 @@ class RenderFlexLayout extends RenderLayoutBox { totalSpace += (itemCount - 1) * mainAxisGap; } - final double freeSpace = maxMainSize != null ? (maxMainSize - totalSpace) : 0.0; + final double freeSpace = + maxMainSize != null ? (maxMainSize - totalSpace) : 0.0; final bool boundedOnly = maxMainSize != null && !(isHorizontal @@ -4971,7 +5225,8 @@ class RenderFlexLayout extends RenderLayoutBox { (contentConstraints?.hasTightHeight ?? false) || constraints.hasTightHeight)); - final bool willShrink = maxMainSize != null && freeSpace < 0 && metrics.totalFlexShrink > 0; + final bool willShrink = + maxMainSize != null && freeSpace < 0 && metrics.totalFlexShrink > 0; bool willGrow = false; if (metrics.totalFlexGrow > 0 && !boundedOnly) { @@ -5052,7 +5307,9 @@ class RenderFlexLayout extends RenderLayoutBox { final RenderBoxModel? effectiveChild = runChild.effectiveChild; if (effectiveChild == null) continue; - final double childOldMainSize = _getRunChildMainSize(runChild); + final Size childSize = _getChildSize(child)!; + final double childOldMainSize = + isHorizontal ? childSize.width : childSize.height; final double? desiredPreservedMain = _childrenIntrinsicMainSizes[child]; _FlexAdjustFastPathRelayoutReason? relayoutReason; BoxConstraints? relayoutConstraints; @@ -5083,12 +5340,14 @@ class RenderFlexLayout extends RenderLayoutBox { final bool autoMain = isHorizontal ? effectiveChild.renderStyle.width.isAuto : effectiveChild.renderStyle.height.isAuto; - final bool wasNonTightMain = isHorizontal ? !applied.hasTightWidth : !applied.hasTightHeight; + final bool wasNonTightMain = + isHorizontal ? !applied.hasTightWidth : !applied.hasTightHeight; if (autoMain && wasNonTightMain) { final bool preservedMainMatches = (desiredPreservedMain - childOldMainSize).abs() < 0.5; final bool textOnlySubtree = _hasOnlyTextFlexRelayoutSubtree(child); - final BoxConstraints candidateConstraints = _getChildAdjustedConstraints( + final BoxConstraints candidateConstraints = + _getChildAdjustedConstraints( effectiveChild, null, null, @@ -5098,9 +5357,8 @@ class RenderFlexLayout extends RenderLayoutBox { final double candidateMainMax = isHorizontal ? candidateConstraints.maxWidth : candidateConstraints.maxHeight; - final bool fitsCandidateMain = - !candidateMainMax.isFinite || - candidateMainMax + 0.5 >= childOldMainSize; + final bool fitsCandidateMain = !candidateMainMax.isFinite || + candidateMainMax + 0.5 >= childOldMainSize; final bool reusesAppliedConstraints = candidateConstraints == applied; final bool tightensToCurrentSize = @@ -5109,15 +5367,14 @@ class RenderFlexLayout extends RenderLayoutBox { candidateConstraints, childOldMainSize, ); - final bool canSkipRelayout = - preservedMainMatches && - (reusesAppliedConstraints || - tightensToCurrentSize || - (textOnlySubtree && fitsCandidateMain)); + final bool canSkipRelayout = preservedMainMatches && + (reusesAppliedConstraints || + tightensToCurrentSize || + (textOnlySubtree && fitsCandidateMain)); if (!canSkipRelayout) { needsLayout = true; - relayoutReason = - _FlexAdjustFastPathRelayoutReason.autoMainWithNonTightConstraint; + relayoutReason = _FlexAdjustFastPathRelayoutReason + .autoMainWithNonTightConstraint; relayoutConstraints = candidateConstraints; } } @@ -5176,12 +5433,12 @@ class RenderFlexLayout extends RenderLayoutBox { final BoxConstraints childConstraints = relayoutConstraints ?? _getChildAdjustedConstraints( - effectiveChild, - null, // no main-axis flexing - null, // no cross-axis stretching - runChildrenCount, - preserveMainAxisSize: desiredPreservedMain, - ); + effectiveChild, + null, // no main-axis flexing + null, // no cross-axis stretching + runChildrenCount, + preserveMainAxisSize: desiredPreservedMain, + ); _layoutChildForFlex(child, childConstraints); _clearSubtreeIntrinsicMeasurementInvalidationAfterMeasurement(child); didRelayout = true; @@ -5196,9 +5453,16 @@ class RenderFlexLayout extends RenderLayoutBox { for (int i = 0; i < runChildrenList.length; i++) { if (i > 0) mainAxisExtent += mainAxisGap; final _RunChild runChild = runChildrenList[i]; - mainAxisExtent += _getRunChildMainAxisExtent(runChild); - crossAxisExtent = - math.max(crossAxisExtent, _getRunChildCrossAxisExtent(runChild)); + final Size childSize = _getChildSize(runChild.child)!; + final double childMainSize = + _isHorizontalFlexDirection ? childSize.width : childSize.height; + final double childCrossSize = + _isHorizontalFlexDirection ? childSize.height : childSize.width; + mainAxisExtent += childMainSize + runChild.mainAxisExtentAdjustment; + crossAxisExtent = math.max( + crossAxisExtent, + childCrossSize + runChild.crossAxisExtentAdjustment, + ); } metrics.mainAxisExtent = mainAxisExtent; metrics.crossAxisExtent = crossAxisExtent; @@ -5217,13 +5481,16 @@ class RenderFlexLayout extends RenderLayoutBox { // Adjust children size (not include position placeholder) based on // flex factors (flex-grow/flex-shrink) and alignment in cross axis (align-items). // https://www.w3.org/TR/css-flexbox-1/#resolve-flexible-lengths - void _adjustChildrenSize(List<_RunMetrics> runMetrics,) { + void _adjustChildrenSize( + List<_RunMetrics> runMetrics, + ) { if (runMetrics.isEmpty) return; final bool isHorizontal = _isHorizontalFlexDirection; final double mainAxisGap = _getMainAxisGap(); final bool hasBaselineAlignment = _hasBaselineAlignedChildren(runMetrics); final bool canAttemptFastPath = isHorizontal && !hasBaselineAlignment; - final bool hasStretchedChildren = _hasStretchedChildrenInCrossAxis(runMetrics); + final bool hasStretchedChildren = + _hasStretchedChildrenInCrossAxis(runMetrics); if (_canSkipAdjustChildrenSize( runMetrics, @@ -5264,7 +5531,8 @@ class RenderFlexLayout extends RenderLayoutBox { double totalSpace = 0; // Flex factor calculation depends on flex-basis if exists. for (final _RunChild runChild in runChildrenList) { - final double childSpace = runChild.usedFlexBasis ?? runChild.originalMainSize; + final double childSpace = + runChild.usedFlexBasis ?? runChild.originalMainSize; totalSpace += childSpace + runChild.mainAxisMargin; } @@ -5279,9 +5547,14 @@ class RenderFlexLayout extends RenderLayoutBox { // For definite main sizes (tight or specified) or auto main size bounded by a max constraint, // distribute free space per spec. Positive free space allows grow when flex-basis is 0 (e.g., flex: 1), // negative free space triggers shrink when items overflow. - final bool boundedOnly = maxMainSize != null && !(isHorizontal - ? (contentBoxLogicalWidth != null || (contentConstraints?.hasTightWidth ?? false) || constraints.hasTightWidth) - : (contentBoxLogicalHeight != null || (contentConstraints?.hasTightHeight ?? false) || constraints.hasTightHeight)); + final bool boundedOnly = maxMainSize != null && + !(isHorizontal + ? (contentBoxLogicalWidth != null || + (contentConstraints?.hasTightWidth ?? false) || + constraints.hasTightWidth) + : (contentBoxLogicalHeight != null || + (contentConstraints?.hasTightHeight ?? false) || + constraints.hasTightHeight)); double initialFreeSpace = 0; if (maxMainSize != null) { initialFreeSpace = maxMainSize - totalSpace; @@ -5299,16 +5572,19 @@ class RenderFlexLayout extends RenderLayoutBox { // Only honor a definite author-specified min-main-size on the container. double containerStyleMin = 0.0; if (isHorizontal) { - if (renderStyle.minWidth.isNotAuto) containerStyleMin = renderStyle.minWidth.computedValue; + if (renderStyle.minWidth.isNotAuto) + containerStyleMin = renderStyle.minWidth.computedValue; } else { - if (renderStyle.minHeight.isNotAuto) containerStyleMin = renderStyle.minHeight.computedValue; + if (renderStyle.minHeight.isNotAuto) + containerStyleMin = renderStyle.minHeight.computedValue; } // If a definite min is set, treat that as the available main size headroom. // Otherwise, keep the container's main size content-driven with zero // distributable positive free space. if (containerStyleMin > 0) { - final double inferredMain = math.max(containerStyleMin, layoutContentMainSize); + final double inferredMain = + math.max(containerStyleMin, layoutContentMainSize); maxMainSize = inferredMain; initialFreeSpace = inferredMain - totalSpace; } else { @@ -5318,15 +5594,18 @@ class RenderFlexLayout extends RenderLayoutBox { } } - double layoutContentMainSize = isHorizontal ? contentSize.width : contentSize.height; + double layoutContentMainSize = + isHorizontal ? contentSize.width : contentSize.height; // Only consider an author-specified (definite) min-main-size on the flex container here. // Do not use the automatic min size, which includes padding/border, to synthesize // positive free space; that incorrectly inflates the container (e.g., to 360). double containerStyleMin = 0.0; if (isHorizontal) { - if (renderStyle.minWidth.isNotAuto) containerStyleMin = renderStyle.minWidth.computedValue; + if (renderStyle.minWidth.isNotAuto) + containerStyleMin = renderStyle.minWidth.computedValue; } else { - if (renderStyle.minHeight.isNotAuto) containerStyleMin = renderStyle.minHeight.computedValue; + if (renderStyle.minHeight.isNotAuto) + containerStyleMin = renderStyle.minHeight.computedValue; } // Adapt free space only when the container has a definite CSS min-main-size. if (initialFreeSpace == 0) { @@ -5336,12 +5615,18 @@ class RenderFlexLayout extends RenderLayoutBox { if (maxMainSize < minTarget) { maxMainSize = minTarget; - double maxMainConstraints = - _isHorizontalFlexDirection ? contentConstraints!.maxWidth : contentConstraints!.maxHeight; + double maxMainConstraints = _isHorizontalFlexDirection + ? contentConstraints!.maxWidth + : contentConstraints!.maxHeight; // determining isScrollingContentBox is to reduce the scope of influence - if (renderStyle.isSelfScrollingContainer() && maxMainConstraints.isFinite) { - maxMainSize = totalFlexShrink > 0 ? math.min(maxMainSize, maxMainConstraints) : maxMainSize; - maxMainSize = totalFlexGrow > 0 ? math.max(maxMainSize, maxMainConstraints) : maxMainSize; + if (renderStyle.isSelfScrollingContainer() && + maxMainConstraints.isFinite) { + maxMainSize = totalFlexShrink > 0 + ? math.min(maxMainSize, maxMainConstraints) + : maxMainSize; + maxMainSize = totalFlexGrow > 0 + ? math.max(maxMainSize, maxMainConstraints) + : maxMainSize; } initialFreeSpace = maxMainSize - totalSpace; @@ -5364,9 +5649,11 @@ class RenderFlexLayout extends RenderLayoutBox { // a definite CSS min-main-size exists on the flex container itself. double containerStyleMin = 0.0; if (isHorizontal) { - if (renderStyle.minWidth.isNotAuto) containerStyleMin = renderStyle.minWidth.computedValue; + if (renderStyle.minWidth.isNotAuto) + containerStyleMin = renderStyle.minWidth.computedValue; } else { - if (renderStyle.minHeight.isNotAuto) containerStyleMin = renderStyle.minHeight.computedValue; + if (renderStyle.minHeight.isNotAuto) + containerStyleMin = renderStyle.minHeight.computedValue; } if (containerStyleMin <= 0 || containerStyleMin <= totalSpace) { @@ -5396,7 +5683,8 @@ class RenderFlexLayout extends RenderLayoutBox { flexShrink: metrics.totalFlexShrink, ); // Loop flex items to resolve flexible length of flex items with flex factor. - while (_resolveFlexibleLengths(metrics, totalFlexFactor, usedFreeSpace)) {} + while ( + _resolveFlexibleLengths(metrics, totalFlexFactor, usedFreeSpace)) {} } // Phase 1 — Relayout each item with its resolved main size only. @@ -5410,11 +5698,14 @@ class RenderFlexLayout extends RenderLayoutBox { // Non-RenderBoxModel child: nothing to tighten in phase 1. continue; } - double childOldMainSize = _getRunChildMainSize(runChild); + final Size childSize = _getChildSize(child)!; + double childOldMainSize = + _isHorizontalFlexDirection ? childSize.width : childSize.height; // Determine used main size from the flexible lengths result, if any. double? childFlexedMainSize; - if ((isFlexGrow && runChild.flexGrow > 0) || (isFlexShrink && runChild.flexShrink > 0)) { + if ((isFlexGrow && runChild.flexGrow > 0) || + (isFlexShrink && runChild.flexShrink > 0)) { childFlexedMainSize = runChild.flexedMainSize; } @@ -5426,7 +5717,9 @@ class RenderFlexLayout extends RenderLayoutBox { bool needsLayout = (childFlexedMainSize != null) || (effectiveChild.needsRelayout) || (_childrenRequirePostMeasureLayout[child] == true); - if (!needsLayout && desiredPreservedMain != null && (desiredPreservedMain != childOldMainSize)) { + if (!needsLayout && + desiredPreservedMain != null && + (desiredPreservedMain != childOldMainSize)) { needsLayout = true; } if (!needsLayout && desiredPreservedMain != null) { @@ -5450,7 +5743,8 @@ class RenderFlexLayout extends RenderLayoutBox { if (!needsLayout && !_isHorizontalFlexDirection) { final bool childCrossAuto = effectiveChild.renderStyle.width.isAuto; final bool noStretch = !_needToStretchChildCrossSize(effectiveChild); - final double availCross = contentConstraints?.maxWidth ?? double.infinity; + final double availCross = + contentConstraints?.maxWidth ?? double.infinity; if (childCrossAuto && noStretch && availCross.isFinite) { // Compare against the border-box width measured during intrinsic pass. final double measuredBorderW = _getChildSize(effectiveChild)!.width; @@ -5494,9 +5788,10 @@ class RenderFlexLayout extends RenderLayoutBox { double? childStretchedCrossSize; if (_needToStretchChildCrossSize(effectiveChild)) { - childStretchedCrossSize = - _getChildStretchedCrossSize(effectiveChild, metrics.crossAxisExtent, runBetweenSpace); - if (effectiveChild is RenderLayoutBox && effectiveChild.isNegativeMarginChangeHSize) { + childStretchedCrossSize = _getChildStretchedCrossSize( + effectiveChild, metrics.crossAxisExtent, runBetweenSpace); + if (effectiveChild is RenderLayoutBox && + effectiveChild.isNegativeMarginChangeHSize) { double childCrossAxisMargin = _isHorizontalFlexDirection ? effectiveChild.renderStyle.margin.vertical : effectiveChild.renderStyle.margin.horizontal; @@ -5508,9 +5803,9 @@ class RenderFlexLayout extends RenderLayoutBox { if (childStretchedCrossSize == null) continue; // If the current cross size already matches the stretched result, skip. + final Size childSize = _getChildSize(child)!; final double currentCross = - _getRunChildCrossAxisExtent(runChild) - - runChild.crossAxisExtentAdjustment; + _isHorizontalFlexDirection ? childSize.height : childSize.width; if ((childStretchedCrossSize - currentCross).abs() < 0.5) continue; // Apply stretch by relayout with tightened cross-axis constraint. @@ -5534,7 +5829,11 @@ class RenderFlexLayout extends RenderLayoutBox { double mainAxisExtent = 0; for (int i = 0; i < runChildrenList.length; i++) { if (i > 0) mainAxisExtent += mainAxisGap; - mainAxisExtent += _getRunChildMainAxisExtent(runChildrenList[i]); + final _RunChild runChild = runChildrenList[i]; + final Size childSize = _getChildSize(runChild.child)!; + final double childMainSize = + _isHorizontalFlexDirection ? childSize.width : childSize.height; + mainAxisExtent += childMainSize + runChild.mainAxisExtentAdjustment; } metrics.mainAxisExtent = mainAxisExtent; metrics.crossAxisExtent = _recomputeRunCrossExtent(metrics); @@ -5576,8 +5875,7 @@ class RenderFlexLayout extends RenderLayoutBox { // Vertical align is only valid for inline box. // Baseline alignment in column direction behave the same as flex-start. AlignSelf alignSelf = runChild.alignSelf; - bool isBaselineAlign = - alignSelf == AlignSelf.baseline || + bool isBaselineAlign = alignSelf == AlignSelf.baseline || alignSelf == AlignSelf.lastBaseline || renderStyle.alignItems == AlignItems.baseline || renderStyle.alignItems == AlignItems.lastBaseline; @@ -5610,7 +5908,8 @@ class RenderFlexLayout extends RenderLayoutBox { // Get constraints of flex items which needs to change size due to // flex-grow/flex-shrink or align-items stretch. - BoxConstraints _getChildAdjustedConstraints(RenderBoxModel child, + BoxConstraints _getChildAdjustedConstraints( + RenderBoxModel child, double? childFlexedMainSize, double? childStretchedCrossSize, int lineChildrenCount, @@ -5634,12 +5933,15 @@ class RenderFlexLayout extends RenderLayoutBox { } } - if (child.renderStyle.isSelfRenderReplaced() && child.renderStyle.aspectRatio != null) { - _overrideReplacedChildLength(child, childFlexedMainSize, childStretchedCrossSize); + if (child.renderStyle.isSelfRenderReplaced() && + child.renderStyle.aspectRatio != null) { + _overrideReplacedChildLength( + child, childFlexedMainSize, childStretchedCrossSize); } // Use stored percentage constraints if available, otherwise use current constraints - BoxConstraints oldConstraints = _childrenOldConstraints[child] ?? child.constraints; + BoxConstraints oldConstraints = + _childrenOldConstraints[child] ?? child.constraints; final bool canUseAdjustedConstraintsCache = _adjustedConstraintsCachePassId == renderBoxModelLayoutPassId; final bool hasOverrideContentLogicalWidth = @@ -5724,8 +6026,9 @@ class RenderFlexLayout extends RenderLayoutBox { if (contentW != null && contentW.isFinite) { final double pad = child.renderStyle.paddingLeft.computedValue + child.renderStyle.paddingRight.computedValue; - final double border = child.renderStyle.effectiveBorderLeftWidth.computedValue + - child.renderStyle.effectiveBorderRightWidth.computedValue; + final double border = + child.renderStyle.effectiveBorderLeftWidth.computedValue + + child.renderStyle.effectiveBorderRightWidth.computedValue; return math.max(0, contentW + pad + border); } return math.max(0, oldConstraints.maxWidth); @@ -5738,8 +6041,9 @@ class RenderFlexLayout extends RenderLayoutBox { if (contentH != null && contentH.isFinite) { final double pad = child.renderStyle.paddingTop.computedValue + child.renderStyle.paddingBottom.computedValue; - final double border = child.renderStyle.effectiveBorderTopWidth.computedValue + - child.renderStyle.effectiveBorderBottomWidth.computedValue; + final double border = + child.renderStyle.effectiveBorderTopWidth.computedValue + + child.renderStyle.effectiveBorderBottomWidth.computedValue; return math.max(0, contentH + pad + border); } return math.max(0, oldConstraints.maxHeight); @@ -5754,10 +6058,14 @@ class RenderFlexLayout extends RenderLayoutBox { double minConstraintWidth = child.hasOverrideContentLogicalWidth ? safeUsedBorderBoxWidth() - : (oldConstraints.minWidth > maxConstraintWidth ? maxConstraintWidth : oldConstraints.minWidth); + : (oldConstraints.minWidth > maxConstraintWidth + ? maxConstraintWidth + : oldConstraints.minWidth); double minConstraintHeight = child.hasOverrideContentLogicalHeight ? safeUsedBorderBoxHeight() - : (oldConstraints.minHeight > maxConstraintHeight ? maxConstraintHeight : oldConstraints.minHeight); + : (oldConstraints.minHeight > maxConstraintHeight + ? maxConstraintHeight + : oldConstraints.minHeight); // If the flex item has a definite height in a row-direction container, // lock the child's height to the used border-box height. This prevents @@ -5814,12 +6122,16 @@ class RenderFlexLayout extends RenderLayoutBox { // overflowing. This mirrors practical browser behavior for scrollable widgets when // author intent is flex: 1; min-height: 0 under a max-height container. if (!_isHorizontalFlexDirection) { - final bool containerBounded = (contentConstraints?.hasBoundedHeight ?? false) && - (contentConstraints?.maxHeight.isFinite ?? false); + final bool containerBounded = + (contentConstraints?.hasBoundedHeight ?? false) && + (contentConstraints?.maxHeight.isFinite ?? false); if (containerBounded) { final double cap = contentConstraints!.maxHeight; - final bool childIsWidget = child is RenderWidget || child.renderStyle.target is WidgetElement; - if (childIsWidget && preserveMainAxisSize != null && preserveMainAxisSize > cap) { + final bool childIsWidget = + child is RenderWidget || child.renderStyle.target is WidgetElement; + if (childIsWidget && + preserveMainAxisSize != null && + preserveMainAxisSize > cap) { minConstraintHeight = cap; maxConstraintHeight = cap; } @@ -5833,9 +6145,12 @@ class RenderFlexLayout extends RenderLayoutBox { // Do not apply this optimization to replaced elements; their aspect-ratio handling // and intrinsic sizing already provide stable behavior, and locking can produce // intermediate widths that conflict with later stretch. - if (!_isHorizontalFlexDirection && childStretchedCrossSize == null && !child.renderStyle.isSelfRenderReplaced()) { + if (!_isHorizontalFlexDirection && + childStretchedCrossSize == null && + !child.renderStyle.isSelfRenderReplaced()) { final bool childCrossAuto = child.renderStyle.width.isAuto; - final bool childCrossPercent = child.renderStyle.width.type == CSSLengthType.PERCENTAGE; + final bool childCrossPercent = + child.renderStyle.width.type == CSSLengthType.PERCENTAGE; // Determine the effective cross-axis alignment for this child. final AlignSelf selfAlign = _getAlignSelf(child); @@ -5849,7 +6164,8 @@ class RenderFlexLayout extends RenderLayoutBox { // column-direction flex items with non-stretch alignment. if (childCrossAuto && !isStretchAlignment) { final WhiteSpace ws = child.renderStyle.whiteSpace; - final bool allowOverflowCross = ws == WhiteSpace.pre || ws == WhiteSpace.nowrap; + final bool allowOverflowCross = + ws == WhiteSpace.pre || ws == WhiteSpace.nowrap; if (allowOverflowCross) { // For unbreakable text (`pre`/`nowrap`), let the item overflow the flex container in // the cross axis by giving it an unbounded width constraint. This allows the span @@ -5861,15 +6177,18 @@ class RenderFlexLayout extends RenderLayoutBox { // Compute shrink-to-fit width in the cross axis for column flex items: // used = min(max(min-content, available), max-content). // Work in content-box, then convert to border-box by adding padding+border. - final double paddingBorderH = child.renderStyle.padding.horizontal + child.renderStyle.border.horizontal; + final double paddingBorderH = child.renderStyle.padding.horizontal + + child.renderStyle.border.horizontal; // Min-content contribution (content-box). final double minContentCB = child.minContentWidth; // Max-content contribution (content-box). Prefer IFC paragraph max-intrinsic width for flow content. double maxContentCB = minContentCB; // fallback - if (child is RenderFlowLayout && child.inlineFormattingContext != null) { - final double paraMax = child.inlineFormattingContext!.paragraphMaxIntrinsicWidth; + if (child is RenderFlowLayout && + child.inlineFormattingContext != null) { + final double paraMax = + child.inlineFormattingContext!.paragraphMaxIntrinsicWidth; if (paraMax.isFinite && paraMax > 0) maxContentCB = paraMax; } @@ -5879,20 +6198,25 @@ class RenderFlexLayout extends RenderLayoutBox { // cross axis. Subtract positive start/end margins to get the space // available to the item’s border-box for shrink-to-fit width. double availableCross = double.infinity; - if (contentConstraints != null && contentConstraints!.maxWidth.isFinite) { + if (contentConstraints != null && + contentConstraints!.maxWidth.isFinite) { availableCross = contentConstraints!.maxWidth; } else { // Fallback to current laid-out content width when known. - final double borderH = renderStyle.effectiveBorderLeftWidth.computedValue + - renderStyle.effectiveBorderRightWidth.computedValue; + final double borderH = + renderStyle.effectiveBorderLeftWidth.computedValue + + renderStyle.effectiveBorderRightWidth.computedValue; final double fallback = math.max(0.0, size.width - borderH); if (fallback.isFinite && fallback > 0) availableCross = fallback; } // Subtract cross-axis margins (positive values only) from available width. if (availableCross.isFinite) { - final double startMargin = _flowAwareChildCrossAxisMargin(child) ?? 0; - final double endMargin = _flowAwareChildCrossAxisMargin(child, isEnd: true) ?? 0; - final double marginDeduction = math.max(0.0, startMargin) + math.max(0.0, endMargin); + final double startMargin = + _flowAwareChildCrossAxisMargin(child) ?? 0; + final double endMargin = + _flowAwareChildCrossAxisMargin(child, isEnd: true) ?? 0; + final double marginDeduction = + math.max(0.0, startMargin) + math.max(0.0, endMargin); availableCross = math.max(0.0, availableCross - marginDeduction); } @@ -5902,8 +6226,11 @@ class RenderFlexLayout extends RenderLayoutBox { // regressing centering cases (e.g., column-wrap with align-self:center). if (maxContentCB <= minContentCB + 0.5) { final double priorBorderW = _getChildSize(child)!.width; - final double priorContentW = - math.max(0.0, priorBorderW - (child.renderStyle.padding.horizontal + child.renderStyle.border.horizontal)); + final double priorContentW = math.max( + 0.0, + priorBorderW - + (child.renderStyle.padding.horizontal + + child.renderStyle.border.horizontal)); if (priorContentW.isFinite && priorContentW > minContentCB) { maxContentCB = priorContentW; } @@ -5911,12 +6238,15 @@ class RenderFlexLayout extends RenderLayoutBox { // Convert to border-box for comparison with constraints we apply to the child. final double minBorder = math.max(0.0, minContentCB + paddingBorderH); - final double maxBorder = math.max(minBorder, maxContentCB + paddingBorderH); - final double availBorder = availableCross.isFinite ? availableCross : double.infinity; + final double maxBorder = + math.max(minBorder, maxContentCB + paddingBorderH); + final double availBorder = + availableCross.isFinite ? availableCross : double.infinity; double shrinkBorderW = maxBorder; if (availBorder.isFinite) { - shrinkBorderW = math.min(math.max(minBorder, availBorder), maxBorder); + shrinkBorderW = + math.min(math.max(minBorder, availBorder), maxBorder); } // Respect the child’s own min/max-width caps. @@ -5925,21 +6255,25 @@ class RenderFlexLayout extends RenderLayoutBox { if (child.renderStyle.minWidth.isNotAuto) { if (child.renderStyle.minWidth.type == CSSLengthType.PERCENTAGE) { if (availableCross.isFinite) { - final double usedMin = (child.renderStyle.minWidth.value ?? 0) * availableCross; + final double usedMin = + (child.renderStyle.minWidth.value ?? 0) * availableCross; shrinkBorderW = math.max(shrinkBorderW, usedMin); } } else { - shrinkBorderW = math.max(shrinkBorderW, child.renderStyle.minWidth.computedValue); + shrinkBorderW = math.max( + shrinkBorderW, child.renderStyle.minWidth.computedValue); } } if (child.renderStyle.maxWidth.isNotNone) { if (child.renderStyle.maxWidth.type == CSSLengthType.PERCENTAGE) { if (availableCross.isFinite) { - final double usedMax = (child.renderStyle.maxWidth.value ?? 0) * availableCross; + final double usedMax = + (child.renderStyle.maxWidth.value ?? 0) * availableCross; shrinkBorderW = math.min(shrinkBorderW, usedMax); } } else { - shrinkBorderW = math.min(shrinkBorderW, child.renderStyle.maxWidth.computedValue); + shrinkBorderW = math.min( + shrinkBorderW, child.renderStyle.maxWidth.computedValue); } } @@ -5954,17 +6288,24 @@ class RenderFlexLayout extends RenderLayoutBox { // (or when alignment falls back to stretch). This preserves previous layout for // stretch cases and avoids regressions. double fixedW; - if (child.renderStyle.isSelfRenderReplaced() && child.renderStyle.aspectRatio != null) { - final double usedBorderBoxH = child.renderStyle.borderBoxLogicalHeight ?? _getChildSize(child)!.height; + if (child.renderStyle.isSelfRenderReplaced() && + child.renderStyle.aspectRatio != null) { + final double usedBorderBoxH = + child.renderStyle.borderBoxLogicalHeight ?? + _getChildSize(child)!.height; fixedW = usedBorderBoxH * child.renderStyle.aspectRatio!; } else { fixedW = _getChildSize(child)!.width; } - final double containerCrossMax = contentConstraints?.maxWidth ?? double.infinity; + final double containerCrossMax = + contentConstraints?.maxWidth ?? double.infinity; final double containerContentW = containerCrossMax.isFinite ? containerCrossMax - : math.max(0.0, size.width - (renderStyle.effectiveBorderLeftWidth.computedValue + - renderStyle.effectiveBorderRightWidth.computedValue)); + : math.max( + 0.0, + size.width - + (renderStyle.effectiveBorderLeftWidth.computedValue + + renderStyle.effectiveBorderRightWidth.computedValue)); double styleMinW = 0.0; final CSSLengthValue minWLen = child.renderStyle.minWidth; @@ -5982,7 +6323,8 @@ class RenderFlexLayout extends RenderLayoutBox { } fixedW = fixedW.clamp(styleMinW, styleMaxW); - if (containerContentW.isFinite) fixedW = math.min(fixedW, containerContentW); + if (containerContentW.isFinite) + fixedW = math.min(fixedW, containerContentW); if (fixedW.isFinite && fixedW > 0) { minConstraintWidth = fixedW; @@ -5994,22 +6336,29 @@ class RenderFlexLayout extends RenderLayoutBox { // once it becomes known (second layout pass). This matches CSS flex item percentage resolution // in column-direction containers. double containerContentW; - if (contentConstraints != null && contentConstraints!.maxWidth.isFinite) { + if (contentConstraints != null && + contentConstraints!.maxWidth.isFinite) { containerContentW = contentConstraints!.maxWidth; } else { - final double borderH = renderStyle.effectiveBorderLeftWidth.computedValue + - renderStyle.effectiveBorderRightWidth.computedValue; + final double borderH = + renderStyle.effectiveBorderLeftWidth.computedValue + + renderStyle.effectiveBorderRightWidth.computedValue; containerContentW = math.max(0, size.width - borderH); } final double percent = child.renderStyle.width.value ?? 0; - double childBorderBoxW = containerContentW.isFinite ? (containerContentW * percent) : 0; - if (!childBorderBoxW.isFinite || childBorderBoxW < 0) childBorderBoxW = 0; + double childBorderBoxW = + containerContentW.isFinite ? (containerContentW * percent) : 0; + if (!childBorderBoxW.isFinite || childBorderBoxW < 0) + childBorderBoxW = 0; - if (child.renderStyle.maxWidth.isNotNone && child.renderStyle.maxWidth.type != CSSLengthType.PERCENTAGE) { - childBorderBoxW = math.min(childBorderBoxW, child.renderStyle.maxWidth.computedValue); + if (child.renderStyle.maxWidth.isNotNone && + child.renderStyle.maxWidth.type != CSSLengthType.PERCENTAGE) { + childBorderBoxW = math.min( + childBorderBoxW, child.renderStyle.maxWidth.computedValue); } if (child.renderStyle.minWidth.isNotAuto) { - childBorderBoxW = math.max(childBorderBoxW, child.renderStyle.minWidth.computedValue); + childBorderBoxW = math.max( + childBorderBoxW, child.renderStyle.minWidth.computedValue); } minConstraintWidth = childBorderBoxW; @@ -6021,10 +6370,14 @@ class RenderFlexLayout extends RenderLayoutBox { // For replaced elements in a column flex container during phase 1 (no cross stretch yet), // cap the available cross size by the container’s content-box width so the intrinsic sizing // does not expand to unconstrained widths (e.g., viewport width) before the stretch phase. - if (!_isHorizontalFlexDirection && child.renderStyle.isSelfRenderReplaced() && childStretchedCrossSize == null) { - final double containerCrossMax = contentConstraints?.maxWidth ?? double.infinity; + if (!_isHorizontalFlexDirection && + child.renderStyle.isSelfRenderReplaced() && + childStretchedCrossSize == null) { + final double containerCrossMax = + contentConstraints?.maxWidth ?? double.infinity; if (containerCrossMax.isFinite) { - if (maxConstraintWidth.isInfinite || maxConstraintWidth > containerCrossMax) { + if (maxConstraintWidth.isInfinite || + maxConstraintWidth > containerCrossMax) { maxConstraintWidth = containerCrossMax; } if (minConstraintWidth > maxConstraintWidth) { @@ -6036,19 +6389,22 @@ class RenderFlexLayout extends RenderLayoutBox { // For elements or any inline-level elements in horizontal flex layout, // avoid tight height constraints during secondary layout passes. // This allows text to properly reflow and adjust its height when width changes. - bool isTextElement = child.renderStyle.isSelfRenderWidget() && child.renderStyle.target is WebFTextElement; - bool isInlineElementWithText = (child.renderStyle.display == CSSDisplay.inline || - child.renderStyle.display == CSSDisplay.inlineBlock || - child.renderStyle.display == CSSDisplay.inlineFlex) && - (child.renderStyle.isSelfRenderFlowLayout() || child.renderStyle.isSelfRenderFlexLayout()); - bool isAutoHeightLayoutContainer = - child.renderStyle.height.isAuto && + bool isTextElement = child.renderStyle.isSelfRenderWidget() && + child.renderStyle.target is WebFTextElement; + bool isInlineElementWithText = + (child.renderStyle.display == CSSDisplay.inline || + child.renderStyle.display == CSSDisplay.inlineBlock || + child.renderStyle.display == CSSDisplay.inlineFlex) && + (child.renderStyle.isSelfRenderFlowLayout() || + child.renderStyle.isSelfRenderFlexLayout()); + bool isAutoHeightLayoutContainer = child.renderStyle.height.isAuto && (child.renderStyle.isSelfRenderFlowLayout() || child.renderStyle.isSelfRenderFlexLayout()); // Block-level flex items whose contents form an inline formatting context (e.g., a
with only text) // also need height to be unconstrained on the secondary pass so text can wrap after flex-shrink. // This mirrors browser behavior: first resolve the used main size, then measure cross size with auto height. - bool establishesIFC = child.renderStyle.shouldEstablishInlineFormattingContext(); + bool establishesIFC = + child.renderStyle.shouldEstablishInlineFormattingContext(); bool isSecondaryLayoutPass = child.hasSize; // Allow dynamic height adjustment during secondary layout when width has changed and height is auto @@ -6060,7 +6416,8 @@ class RenderFlexLayout extends RenderLayoutBox { isAutoHeightLayoutContainer) && // For non-flexed items, only allow when this is the only item on the line // so the line cross-size is content-driven. - (childFlexedMainSize != null || (preserveMainAxisSize != null && lineChildrenCount == 1)) && + (childFlexedMainSize != null || + (preserveMainAxisSize != null && lineChildrenCount == 1)) && // Layout containers with auto height need a chance to grow past the // previous stretched cross size when their own contents change. (childStretchedCrossSize == null || @@ -6074,7 +6431,8 @@ class RenderFlexLayout extends RenderLayoutBox { // margins and padding are preserved while still allowing it to expand beyond // the stretched size if its contents require it. if (childStretchedCrossSize != null && childStretchedCrossSize > 0) { - minConstraintHeight = math.max(minConstraintHeight, childStretchedCrossSize); + minConstraintHeight = + math.max(minConstraintHeight, childStretchedCrossSize); } else { minConstraintHeight = 0; } @@ -6094,8 +6452,10 @@ class RenderFlexLayout extends RenderLayoutBox { if (!child.renderStyle.paddingBottom.isAuto) { contentMinHeight += child.renderStyle.paddingBottom.computedValue; } - contentMinHeight += child.renderStyle.effectiveBorderTopWidth.computedValue; - contentMinHeight += child.renderStyle.effectiveBorderBottomWidth.computedValue; + contentMinHeight += + child.renderStyle.effectiveBorderTopWidth.computedValue; + contentMinHeight += + child.renderStyle.effectiveBorderBottomWidth.computedValue; // Allow child to expand beyond parent's maxHeight if content requires it // This matches browser behavior where content can overflow constrained parents @@ -6139,19 +6499,23 @@ class RenderFlexLayout extends RenderLayoutBox { final bool isReplaced = child.renderStyle.isSelfRenderReplaced(); final double childFlexGrow = _getFlexGrow(child); final double childFlexShrink = _getFlexShrink(child); - final bool isFlexNone = childFlexGrow == 0 && childFlexShrink == 0; // flex: none - if (hasDefiniteFlexBasis || (child.renderStyle.width.isAuto && !isReplaced)) { + final bool isFlexNone = + childFlexGrow == 0 && childFlexShrink == 0; // flex: none + if (hasDefiniteFlexBasis || + (child.renderStyle.width.isAuto && !isReplaced)) { if (isFlexNone) { // For flex: none items, do not constrain to the container width. // Let the item keep its preserved (intrinsic) width and overflow if necessary. minConstraintWidth = preserveMainAxisSize; maxConstraintWidth = preserveMainAxisSize; } else { - final double containerAvail = contentConstraints?.maxWidth ?? double.infinity; + final double containerAvail = + contentConstraints?.maxWidth ?? double.infinity; if (containerAvail.isFinite) { double cap = preserveMainAxisSize; // Also honor the child’s own definite (non-percentage) max-width if any. - if (child.renderStyle.maxWidth.isNotNone && child.renderStyle.maxWidth.type != CSSLengthType.PERCENTAGE) { + if (child.renderStyle.maxWidth.isNotNone && + child.renderStyle.maxWidth.type != CSSLengthType.PERCENTAGE) { cap = math.min(cap, child.renderStyle.maxWidth.computedValue); } cap = math.min(cap, containerAvail); @@ -6174,9 +6538,10 @@ class RenderFlexLayout extends RenderLayoutBox { // container must not force a tight height on the item; allow the child to // reflow under the container's bounded height instead of freezing to the // intrinsic height from PASS 2. - final bool containerBoundedOnly = (contentConstraints?.hasBoundedHeight ?? false) && - !(contentConstraints?.hasTightHeight ?? false) && - renderStyle.contentBoxLogicalHeight == null; + final bool containerBoundedOnly = + (contentConstraints?.hasBoundedHeight ?? false) && + !(contentConstraints?.hasTightHeight ?? false) && + renderStyle.contentBoxLogicalHeight == null; // Avoid over-constraining text reflow cases by applying only when the // intrinsic pass forced a tight zero height or when the basis is definite @@ -6212,9 +6577,11 @@ class RenderFlexLayout extends RenderLayoutBox { // When replaced element is stretched or shrinked only on one axis and // length is not specified on the other axis, the length needs to be // overrided in the other axis. - void _overrideReplacedChildLength(RenderBoxModel child, - double? childFlexedMainSize, - double? childStretchedCrossSize,) { + void _overrideReplacedChildLength( + RenderBoxModel child, + double? childFlexedMainSize, + double? childStretchedCrossSize, + ) { assert(child.renderStyle.isSelfRenderReplaced()); if (childFlexedMainSize != null && childStretchedCrossSize == null) { if (_isHorizontalFlexDirection) { @@ -6234,38 +6601,48 @@ class RenderFlexLayout extends RenderLayoutBox { } // Override replaced child height when its height is auto. - void _overrideReplacedChildHeight(RenderBoxModel child,) { + void _overrideReplacedChildHeight( + RenderBoxModel child, + ) { assert(child.renderStyle.isSelfRenderReplaced()); if (child.renderStyle.height.isAuto) { double maxConstraintWidth = child.renderStyle.borderBoxLogicalWidth!; - double maxConstraintHeight = maxConstraintWidth / child.renderStyle.aspectRatio!; + double maxConstraintHeight = + maxConstraintWidth / child.renderStyle.aspectRatio!; // Clamp replaced element height by min/max height. if (child.renderStyle.minHeight.isNotAuto) { double minHeight = child.renderStyle.minHeight.computedValue; - maxConstraintHeight = maxConstraintHeight < minHeight ? minHeight : maxConstraintHeight; + maxConstraintHeight = + maxConstraintHeight < minHeight ? minHeight : maxConstraintHeight; } if (child.renderStyle.maxHeight.isNotNone) { double maxHeight = child.renderStyle.maxHeight.computedValue; - maxConstraintHeight = maxConstraintHeight > maxHeight ? maxHeight : maxConstraintHeight; + maxConstraintHeight = + maxConstraintHeight > maxHeight ? maxHeight : maxConstraintHeight; } _overrideChildContentBoxLogicalHeight(child, maxConstraintHeight); } } // Override replaced child width when its width is auto. - void _overrideReplacedChildWidth(RenderBoxModel child,) { + void _overrideReplacedChildWidth( + RenderBoxModel child, + ) { assert(child.renderStyle.isSelfRenderReplaced()); if (child.renderStyle.width.isAuto) { double maxConstraintHeight = child.renderStyle.borderBoxLogicalHeight!; - double maxConstraintWidth = maxConstraintHeight * child.renderStyle.aspectRatio!; + double maxConstraintWidth = + maxConstraintHeight * child.renderStyle.aspectRatio!; // Clamp replaced element width by min/max width. if (child.renderStyle.minWidth.isNotAuto) { double minWidth = child.renderStyle.minWidth.computedValue; - maxConstraintWidth = maxConstraintWidth < minWidth ? minWidth : maxConstraintWidth; + maxConstraintWidth = + maxConstraintWidth < minWidth ? minWidth : maxConstraintWidth; } if (child.renderStyle.maxWidth.isNotNone) { double maxWidth = child.renderStyle.maxWidth.computedValue; - maxConstraintWidth = maxConstraintWidth > maxWidth ? maxWidth : maxConstraintWidth; + maxConstraintWidth = + maxConstraintWidth > maxWidth ? maxWidth : maxConstraintWidth; } _overrideChildContentBoxLogicalWidth(child, maxConstraintWidth); } @@ -6273,13 +6650,15 @@ class RenderFlexLayout extends RenderLayoutBox { // Override content box logical width of child when flex-grow/flex-shrink/align-items has changed // child's size. - void _overrideChildContentBoxLogicalWidth(RenderBoxModel child, double maxConstraintWidth) { + void _overrideChildContentBoxLogicalWidth( + RenderBoxModel child, double maxConstraintWidth) { // Deflating padding/border can yield a negative content-box when the // assigned border-box is smaller than padding+border. CSS forbids // negative content sizes; clamp to zero so downstream layout (e.g., // intrinsic measurement and alignment) receives a sane, non-negative // logical size. - double deflated = child.renderStyle.deflatePaddingBorderWidth(maxConstraintWidth); + double deflated = + child.renderStyle.deflatePaddingBorderWidth(maxConstraintWidth); if (deflated.isFinite && deflated < 0) deflated = 0; child.renderStyle.contentBoxLogicalWidth = deflated; child.hasOverrideContentLogicalWidth = true; @@ -6287,18 +6666,22 @@ class RenderFlexLayout extends RenderLayoutBox { // Override content box logical height of child when flex-grow/flex-shrink/align-items has changed // child's size. - void _overrideChildContentBoxLogicalHeight(RenderBoxModel child, double maxConstraintHeight) { + void _overrideChildContentBoxLogicalHeight( + RenderBoxModel child, double maxConstraintHeight) { // See width counterpart: guard against negative content-box heights // when padding/border exceed the assigned border-box. Use zero to // represent the collapsed content box per spec. - double deflated = child.renderStyle.deflatePaddingBorderHeight(maxConstraintHeight); + double deflated = + child.renderStyle.deflatePaddingBorderHeight(maxConstraintHeight); if (deflated.isFinite && deflated < 0) deflated = 0; child.renderStyle.contentBoxLogicalHeight = deflated; child.hasOverrideContentLogicalHeight = true; } // Set flex container size according to children size. - void _setContainerSize(List<_RunMetrics> runMetrics,) { + void _setContainerSize( + List<_RunMetrics> runMetrics, + ) { if (runMetrics.isEmpty) { _setContainerSizeWithNoChild(); return; @@ -6309,13 +6692,16 @@ class RenderFlexLayout extends RenderLayoutBox { // mainAxis gaps are already included in metrics.mainAxisExtent after PASS 3. // No need to add them again as this would double-count and cause incorrect sizing. - double contentWidth = _isHorizontalFlexDirection ? runMaxMainSize : runCrossSize; - double contentHeight = _isHorizontalFlexDirection ? runCrossSize : runMaxMainSize; + double contentWidth = + _isHorizontalFlexDirection ? runMaxMainSize : runCrossSize; + double contentHeight = + _isHorizontalFlexDirection ? runCrossSize : runMaxMainSize; // Respect specified cross size (height for row, width for column) without growing the container. // This allows flex items to overflow when their content is taller/wider than the container. if (_isHorizontalFlexDirection) { - final double? specifiedContentHeight = renderStyle.contentBoxLogicalHeight; + final double? specifiedContentHeight = + renderStyle.contentBoxLogicalHeight; if (specifiedContentHeight != null) { contentHeight = specifiedContentHeight; } @@ -6360,14 +6746,19 @@ class RenderFlexLayout extends RenderLayoutBox { final List<_RunChild> runChildren = runMetrics.runChildren; double runMainExtent = 0; for (final _RunChild runChild in runChildren) { - runMainExtent += _getRunChildMainAxisExtent(runChild); + final Size childSize = _getChildSize(runChild.child)!; + final double childMainSize = + _isHorizontalFlexDirection ? childSize.width : childSize.height; + runMainExtent += childMainSize + runChild.mainAxisExtentAdjustment; } runMainSize.add(runMainExtent); } // Get auto min size in the main axis which equals the main axis size of its contents. // https://www.w3.org/TR/css-sizing-3/#automatic-minimum-size - double _getMainAxisAutoSize(List<_RunMetrics> runMetrics,) { + double _getMainAxisAutoSize( + List<_RunMetrics> runMetrics, + ) { double autoMinSize = 0; // Main size of each run. @@ -6391,7 +6782,8 @@ class RenderFlexLayout extends RenderLayoutBox { for (final _RunChild runChild in runChildren) { final RenderBox child = runChild.child; final Size childSize = _getChildSize(child)!; - final double runChildCrossSize = _isHorizontalFlexDirection ? childSize.height : childSize.width; + final double runChildCrossSize = + _isHorizontalFlexDirection ? childSize.height : childSize.width; runCrossExtent = math.max(runCrossExtent, runChildCrossSize); } runCrossSize.add(runCrossExtent); @@ -6399,7 +6791,9 @@ class RenderFlexLayout extends RenderLayoutBox { // Get auto min size in the cross axis which equals the cross axis size of its contents. // https://www.w3.org/TR/css-sizing-3/#automatic-minimum-size - double _getCrossAxisAutoSize(List<_RunMetrics> runMetrics,) { + double _getCrossAxisAutoSize( + List<_RunMetrics> runMetrics, + ) { double autoMinSize = 0; // Cross size of each run. @@ -6454,7 +6848,8 @@ class RenderFlexLayout extends RenderLayoutBox { final CSSOverflowType overflowX = childRenderStyle.effectiveOverflowX; final CSSOverflowType overflowY = childRenderStyle.effectiveOverflowY; // Only non scroll container need to use scrollable size, otherwise use its own size. - if (overflowX == CSSOverflowType.visible && overflowY == CSSOverflowType.visible) { + if (overflowX == CSSOverflowType.visible && + overflowY == CSSOverflowType.visible) { childScrollableSize = child.scrollableSize; } @@ -6463,33 +6858,42 @@ class RenderFlexLayout extends RenderLayoutBox { // https://www.w3.org/TR/css-overflow-3/#scrollable-overflow-region // Add offset of margin. - childOffsetX += childRenderStyle.marginLeft.computedValue + childRenderStyle.marginRight.computedValue; - childOffsetY += childRenderStyle.marginTop.computedValue + childRenderStyle.marginBottom.computedValue; + childOffsetX += childRenderStyle.marginLeft.computedValue + + childRenderStyle.marginRight.computedValue; + childOffsetY += childRenderStyle.marginTop.computedValue + + childRenderStyle.marginBottom.computedValue; // Add offset of position relative. // Offset of position absolute and fixed is added in layout stage of positioned renderBox. - final Offset? relativeOffset = CSSPositionedLayout.getRelativeOffset(childRenderStyle); + final Offset? relativeOffset = + CSSPositionedLayout.getRelativeOffset(childRenderStyle); if (relativeOffset != null) { childOffsetX += relativeOffset.dx; childOffsetY += relativeOffset.dy; } // Add offset of transform. - final Offset? transformOffset = child.renderStyle.effectiveTransformOffset; + final Offset? transformOffset = + child.renderStyle.effectiveTransformOffset; if (transformOffset != null) { childOffsetX += transformOffset.dx; childOffsetY += transformOffset.dy; childTransformMainOverflow = math.max( 0, - _isHorizontalFlexDirection ? transformOffset.dx : transformOffset.dy, + _isHorizontalFlexDirection + ? transformOffset.dx + : transformOffset.dy, ); } } final Size childSize = _getChildSize(child)!; - final double childBoxMainSize = _isHorizontalFlexDirection ? childSize.width : childSize.height; - final double childBoxCrossSize = _isHorizontalFlexDirection ? childSize.height : childSize.width; - final double childCrossOffset = _isHorizontalFlexDirection ? childOffsetY : childOffsetX; + final double childBoxMainSize = + _isHorizontalFlexDirection ? childSize.width : childSize.height; + final double childBoxCrossSize = + _isHorizontalFlexDirection ? childSize.height : childSize.width; + final double childCrossOffset = + _isHorizontalFlexDirection ? childOffsetY : childOffsetX; final double childScrollableMainExtent = _isHorizontalFlexDirection ? childScrollableSize.width + childTransformMainOverflow : childScrollableSize.height + childTransformMainOverflow; @@ -6514,20 +6918,22 @@ class RenderFlexLayout extends RenderLayoutBox { // instead of the pre-alignment stacked size. This prevents blank trailing // scroll range after children are shifted by negative leading space. final double childScrollableMain = math.max( - 0, - childMainPosition - physicalMainAxisStartBorder, - ) + + 0, + childMainPosition - physicalMainAxisStartBorder, + ) + math.max(childBoxMainSize, childScrollableMainExtent) + childPhysicalMainEndMargin; final double childScrollableCross = math.max( - childBoxCrossSize + childCrossOffset, - childScrollableCrossExtent); + childBoxCrossSize + childCrossOffset, childScrollableCrossExtent); - maxScrollableMainSizeOfLine = math.max(maxScrollableMainSizeOfLine, childScrollableMain); - maxScrollableCrossSizeInLine = math.max(maxScrollableCrossSizeInLine, childScrollableCross); + maxScrollableMainSizeOfLine = + math.max(maxScrollableMainSizeOfLine, childScrollableMain); + maxScrollableCrossSizeInLine = + math.max(maxScrollableCrossSizeInLine, childScrollableCross); // Update running main size for subsequent siblings (border-box size + main-axis margins). - preSiblingsMainSize += _getRunChildMainAxisExtent(runChild); + preSiblingsMainSize += + childBoxMainSize + runChild.mainAxisExtentAdjustment; // Add main-axis gap between items (not after the last item). if (runChild != runChildren.last) { preSiblingsMainSize += _getMainAxisGap(); @@ -6535,7 +6941,8 @@ class RenderFlexLayout extends RenderLayoutBox { } // Max scrollable cross size of all the children in the line. - final double maxScrollableCrossSizeOfLine = preLinesCrossSize + maxScrollableCrossSizeInLine; + final double maxScrollableCrossSizeOfLine = + preLinesCrossSize + maxScrollableCrossSizeInLine; scrollableMainSizeOfLines.add(maxScrollableMainSizeOfLine); scrollableCrossSizeOfLines.add(maxScrollableCrossSizeOfLine); @@ -6550,12 +6957,13 @@ class RenderFlexLayout extends RenderLayoutBox { double maxScrollableMainSizeOfLines = scrollableMainSizeOfLines.isEmpty ? 0 : scrollableMainSizeOfLines.reduce((double curr, double next) { - return curr > next ? curr : next; - }); + return curr > next ? curr : next; + }); RenderBoxModel container = this; - bool isScrollContainer = renderStyle.effectiveOverflowX != CSSOverflowType.visible || - renderStyle.effectiveOverflowY != CSSOverflowType.visible; + bool isScrollContainer = + renderStyle.effectiveOverflowX != CSSOverflowType.visible || + renderStyle.effectiveOverflowY != CSSOverflowType.visible; // Child positions already include physical start padding. Only the trailing // padding needs to be added here. @@ -6566,8 +6974,8 @@ class RenderFlexLayout extends RenderLayoutBox { double maxScrollableCrossSizeOfLines = scrollableCrossSizeOfLines.isEmpty ? 0 : scrollableCrossSizeOfLines.reduce((double curr, double next) { - return curr > next ? curr : next; - }); + return curr > next ? curr : next; + }); // Padding in the end direction of axis should be included in scroll container. double maxScrollableCrossSizeOfChildren = maxScrollableCrossSizeOfLines + @@ -6581,9 +6989,15 @@ class RenderFlexLayout extends RenderLayoutBox { container.renderStyle.effectiveBorderTopWidth.computedValue - container.renderStyle.effectiveBorderBottomWidth.computedValue; double maxScrollableMainSize = math.max( - _isHorizontalFlexDirection ? containerContentWidth : containerContentHeight, maxScrollableMainSizeOfChildren); + _isHorizontalFlexDirection + ? containerContentWidth + : containerContentHeight, + maxScrollableMainSizeOfChildren); double maxScrollableCrossSize = math.max( - _isHorizontalFlexDirection ? containerContentHeight : containerContentWidth, maxScrollableCrossSizeOfChildren); + _isHorizontalFlexDirection + ? containerContentHeight + : containerContentWidth, + maxScrollableCrossSizeOfChildren); scrollableSize = _isHorizontalFlexDirection ? Size(maxScrollableMainSize, maxScrollableCrossSize) @@ -6591,10 +7005,13 @@ class RenderFlexLayout extends RenderLayoutBox { } // Get the cross size of flex line based on flex-wrap and align-items/align-self properties. - double _getFlexLineCrossSize(RenderBox child, - double runCrossAxisExtent, - double runBetweenSpace,) { - bool isSingleLine = (renderStyle.flexWrap != FlexWrap.wrap && renderStyle.flexWrap != FlexWrap.wrapReverse); + double _getFlexLineCrossSize( + RenderBox child, + double runCrossAxisExtent, + double runBetweenSpace, + ) { + bool isSingleLine = (renderStyle.flexWrap != FlexWrap.wrap && + renderStyle.flexWrap != FlexWrap.wrapReverse); if (isSingleLine) { final bool hasDefiniteContainerCross = _hasDefiniteContainerCrossSize(); @@ -6605,11 +7022,14 @@ class RenderFlexLayout extends RenderLayoutBox { // flex containers the flex line’s cross size equals the flex container’s inner cross size // when that size is definite. // https://www.w3.org/TR/css-flexbox-1/#algo-cross-line - double? explicitContainerCross; // from explicit non-auto width/height - double? resolvedContainerCross; // resolved cross size for block-level flex when auto - double? minCrossFromConstraints; // content-box min cross size - double? minCrossFromStyle; // content-box min cross size derived from min-width/min-height - double? containerInnerCross; // measured inner cross size from this layout pass + double? explicitContainerCross; // from explicit non-auto width/height + double? + resolvedContainerCross; // resolved cross size for block-level flex when auto + double? minCrossFromConstraints; // content-box min cross size + double? + minCrossFromStyle; // content-box min cross size derived from min-width/min-height + double? + containerInnerCross; // measured inner cross size from this layout pass final CSSDisplay effectiveDisplay = renderStyle.effectiveDisplay; final bool isInlineFlex = effectiveDisplay == CSSDisplay.inlineFlex; final CSSWritingMode wm = renderStyle.writingMode; @@ -6625,27 +7045,33 @@ class RenderFlexLayout extends RenderLayoutBox { // Row: cross axis is height. final double maxH = constraints.maxHeight; if (maxH.isFinite) { - final double borderV = renderStyle.effectiveBorderTopWidth.computedValue + - renderStyle.effectiveBorderBottomWidth.computedValue; - final double paddingV = renderStyle.paddingTop.computedValue + renderStyle.paddingBottom.computedValue; + final double borderV = + renderStyle.effectiveBorderTopWidth.computedValue + + renderStyle.effectiveBorderBottomWidth.computedValue; + final double paddingV = renderStyle.paddingTop.computedValue + + renderStyle.paddingBottom.computedValue; availableInnerCross = math.max(0, maxH - borderV - paddingV); } } else { // Column: cross axis is width. final double maxW = constraints.maxWidth; if (maxW.isFinite) { - final double borderH = renderStyle.effectiveBorderLeftWidth.computedValue + - renderStyle.effectiveBorderRightWidth.computedValue; - final double paddingH = renderStyle.paddingLeft.computedValue + renderStyle.paddingRight.computedValue; + final double borderH = + renderStyle.effectiveBorderLeftWidth.computedValue + + renderStyle.effectiveBorderRightWidth.computedValue; + final double paddingH = renderStyle.paddingLeft.computedValue + + renderStyle.paddingRight.computedValue; availableInnerCross = math.max(0, maxW - borderH - paddingH); } } } double clampToAvailable(double value) { - if (availableInnerCross == null || !availableInnerCross.isFinite) return value; + if (availableInnerCross == null || !availableInnerCross.isFinite) + return value; return value > availableInnerCross ? availableInnerCross : value; } + if (_isHorizontalFlexDirection) { // Row: cross is height // Only treat as definite if height is explicitly specified (not auto) @@ -6653,7 +7079,9 @@ class RenderFlexLayout extends RenderLayoutBox { explicitContainerCross = renderStyle.contentBoxLogicalHeight; } // Also consider the actually measured inner cross size from this layout pass. - if (hasDefiniteContainerCross && contentSize.height.isFinite && contentSize.height > 0) { + if (hasDefiniteContainerCross && + contentSize.height.isFinite && + contentSize.height > 0) { containerInnerCross = contentSize.height; } // min-height should also participate in establishing the line cross size @@ -6661,14 +7089,17 @@ class RenderFlexLayout extends RenderLayoutBox { // is otherwise indefinite (e.g. height:auto under grid layout constraints). if (renderStyle.minHeight.isNotAuto) { final double minBorderBox = renderStyle.minHeight.computedValue; - double minContentBox = renderStyle.deflatePaddingBorderHeight(minBorderBox); + double minContentBox = + renderStyle.deflatePaddingBorderHeight(minBorderBox); if (minContentBox.isFinite && minContentBox < 0) minContentBox = 0; if (minContentBox.isFinite && minContentBox > 0) { minCrossFromStyle = minContentBox; } } // Height:auto is generally not definite prior to layout; still capture a min-cross constraint if present. - if (contentConstraints != null && contentConstraints!.minHeight.isFinite && contentConstraints!.minHeight > 0) { + if (contentConstraints != null && + contentConstraints!.minHeight.isFinite && + contentConstraints!.minHeight > 0) { minCrossFromConstraints = contentConstraints!.minHeight; } } else { @@ -6679,17 +7110,22 @@ class RenderFlexLayout extends RenderLayoutBox { } // For block-level flex with width:auto in horizontal writing mode, the used width // is fill-available and thus definite; only then may we resolve from constraints. - if (hasDefiniteContainerCross && !isInlineFlex && (explicitContainerCross == null) && crossIsWidth && + if (hasDefiniteContainerCross && + !isInlineFlex && + (explicitContainerCross == null) && + crossIsWidth && wm == CSSWritingMode.horizontalTb) { - if (contentConstraints != null && contentConstraints!.hasBoundedWidth && + if (contentConstraints != null && + contentConstraints!.hasBoundedWidth && contentConstraints!.maxWidth.isFinite) { resolvedContainerCross = contentConstraints!.maxWidth; - } else if (constraints.hasBoundedWidth && constraints.maxWidth.isFinite) { - final double borderH = renderStyle.effectiveBorderLeftWidth.computedValue + - renderStyle.effectiveBorderRightWidth.computedValue; - final double paddingH = - renderStyle.paddingLeft.computedValue + - renderStyle.paddingRight.computedValue; + } else if (constraints.hasBoundedWidth && + constraints.maxWidth.isFinite) { + final double borderH = + renderStyle.effectiveBorderLeftWidth.computedValue + + renderStyle.effectiveBorderRightWidth.computedValue; + final double paddingH = renderStyle.paddingLeft.computedValue + + renderStyle.paddingRight.computedValue; resolvedContainerCross = math.max(0, constraints.maxWidth - borderH - paddingH); } @@ -6698,26 +7134,34 @@ class RenderFlexLayout extends RenderLayoutBox { // content width as a definite line cross size; width:auto should shrink-to-fit // in vertical writing modes. Only consider the measured inner width when the // container has an explicit (non-auto) width. - if (hasDefiniteContainerCross && renderStyle.width.isNotAuto && contentSize.width.isFinite && contentSize.width > 0) { + if (hasDefiniteContainerCross && + renderStyle.width.isNotAuto && + contentSize.width.isFinite && + contentSize.width > 0) { containerInnerCross = contentSize.width; } if (renderStyle.minWidth.isNotAuto) { final double minBorderBox = renderStyle.minWidth.computedValue; - double minContentBox = renderStyle.deflatePaddingBorderWidth(minBorderBox); + double minContentBox = + renderStyle.deflatePaddingBorderWidth(minBorderBox); if (minContentBox.isFinite && minContentBox < 0) minContentBox = 0; if (minContentBox.isFinite && minContentBox > 0) { minCrossFromStyle = minContentBox; } } - if (contentConstraints != null && contentConstraints!.minWidth.isFinite && contentConstraints!.minWidth > 0) { + if (contentConstraints != null && + contentConstraints!.minWidth.isFinite && + contentConstraints!.minWidth > 0) { minCrossFromConstraints = contentConstraints!.minWidth; } } // Prefer the larger of the style-derived and constraints-derived minimum cross sizes. if (minCrossFromStyle != null && minCrossFromStyle!.isFinite) { - if (minCrossFromConstraints != null && minCrossFromConstraints!.isFinite) { - minCrossFromConstraints = math.max(minCrossFromConstraints!, minCrossFromStyle!); + if (minCrossFromConstraints != null && + minCrossFromConstraints!.isFinite) { + minCrossFromConstraints = + math.max(minCrossFromConstraints!, minCrossFromStyle!); } else { minCrossFromConstraints = minCrossFromStyle; } @@ -6742,7 +7186,9 @@ class RenderFlexLayout extends RenderLayoutBox { if (containerInnerCross != null && containerInnerCross.isFinite) { return containerInnerCross; } - if (!isInlineFlex && resolvedContainerCross != null && resolvedContainerCross.isFinite) { + if (!isInlineFlex && + resolvedContainerCross != null && + resolvedContainerCross.isFinite) { return resolvedContainerCross; } } @@ -6753,7 +7199,8 @@ class RenderFlexLayout extends RenderLayoutBox { return runCrossAxisExtent; } else { // Flex line of align-content stretch should includes between space. - bool isMultiLineStretch = renderStyle.alignContent == AlignContent.stretch; + bool isMultiLineStretch = + renderStyle.alignContent == AlignContent.stretch; if (isMultiLineStretch) { return runCrossAxisExtent + runBetweenSpace; } else { @@ -6763,7 +7210,9 @@ class RenderFlexLayout extends RenderLayoutBox { } // Set children offset based on alignment properties. - void _setChildrenOffset(List<_RunMetrics> runMetrics,) { + void _setChildrenOffset( + List<_RunMetrics> runMetrics, + ) { if (runMetrics.isEmpty) return; final bool isHorizontal = _isHorizontalFlexDirection; @@ -6802,19 +7251,23 @@ class RenderFlexLayout extends RenderLayoutBox { // intrinsic contentSize to the actual inner box size from Flutter // constraints so that alignment (justify-content/align-items) operates // within the visible box instead of an unconstrained CSS height/width. - final double borderLeft = renderStyle.effectiveBorderLeftWidth.computedValue; - final double borderRight = renderStyle.effectiveBorderRightWidth.computedValue; - final double borderTop = renderStyle.effectiveBorderTopWidth.computedValue; - final double borderBottom = renderStyle.effectiveBorderBottomWidth.computedValue; + final double borderLeft = + renderStyle.effectiveBorderLeftWidth.computedValue; + final double borderRight = + renderStyle.effectiveBorderRightWidth.computedValue; + final double borderTop = + renderStyle.effectiveBorderTopWidth.computedValue; + final double borderBottom = + renderStyle.effectiveBorderBottomWidth.computedValue; final double paddingLeft = renderStyle.paddingLeft.computedValue; final double paddingRight = renderStyle.paddingRight.computedValue; final double paddingTop = renderStyle.paddingTop.computedValue; final double paddingBottom = renderStyle.paddingBottom.computedValue; - final double innerWidthFromSize = - math.max(0.0, size.width - borderLeft - borderRight - paddingLeft - paddingRight); - final double innerHeightFromSize = - math.max(0.0, size.height - borderTop - borderBottom - paddingTop - paddingBottom); + final double innerWidthFromSize = math.max(0.0, + size.width - borderLeft - borderRight - paddingLeft - paddingRight); + final double innerHeightFromSize = math.max(0.0, + size.height - borderTop - borderBottom - paddingTop - paddingBottom); double contentMainExtent; double contentCrossExtent; @@ -6840,7 +7293,8 @@ class RenderFlexLayout extends RenderLayoutBox { if (!mainAxisContentSize.isFinite || mainAxisContentSize <= 0) { mainAxisContentSize = innerMainFromSize; } else { - mainAxisContentSize = math.min(mainAxisContentSize, innerMainFromSize); + mainAxisContentSize = + math.min(mainAxisContentSize, innerMainFromSize); } } crossAxisContentSize = contentCrossExtent; @@ -6848,7 +7302,8 @@ class RenderFlexLayout extends RenderLayoutBox { if (!crossAxisContentSize.isFinite || crossAxisContentSize <= 0) { crossAxisContentSize = innerCrossFromSize; } else { - crossAxisContentSize = math.min(crossAxisContentSize, innerCrossFromSize); + crossAxisContentSize = + math.min(crossAxisContentSize, innerCrossFromSize); } } } else { @@ -6899,7 +7354,9 @@ class RenderFlexLayout extends RenderLayoutBox { if (remainingSpace < 0) { betweenSpace = 0.0; } else { - betweenSpace = runChildrenCount > 1 ? remainingSpace / (runChildrenCount - 1) : 0.0; + betweenSpace = runChildrenCount > 1 + ? remainingSpace / (runChildrenCount - 1) + : 0.0; } break; case JustifyContent.spaceAround: @@ -6907,7 +7364,8 @@ class RenderFlexLayout extends RenderLayoutBox { leadingSpace = remainingSpace / 2.0; betweenSpace = 0.0; } else { - betweenSpace = runChildrenCount > 0 ? remainingSpace / runChildrenCount : 0.0; + betweenSpace = + runChildrenCount > 0 ? remainingSpace / runChildrenCount : 0.0; leadingSpace = betweenSpace / 2.0; } break; @@ -6916,7 +7374,9 @@ class RenderFlexLayout extends RenderLayoutBox { leadingSpace = remainingSpace / 2.0; betweenSpace = 0.0; } else { - betweenSpace = runChildrenCount > 0 ? remainingSpace / (runChildrenCount + 1) : 0.0; + betweenSpace = runChildrenCount > 0 + ? remainingSpace / (runChildrenCount + 1) + : 0.0; leadingSpace = betweenSpace; } break; @@ -6942,17 +7402,23 @@ class RenderFlexLayout extends RenderLayoutBox { // Main axis position of child on layout. double childMainPosition = flipMainAxis - ? mainAxisStartPadding + mainAxisStartBorder + mainAxisContentSize - leadingSpace + ? mainAxisStartPadding + + mainAxisStartBorder + + mainAxisContentSize - + leadingSpace : leadingSpace + mainAxisStartPadding + mainAxisStartBorder; - final bool runAllChildrenAtMaxCross = _areAllRunChildrenAtMaxCrossExtent(runChildrenList, runCrossAxisExtent); + final bool runAllChildrenAtMaxCross = _areAllRunChildrenAtMaxCrossExtent( + runChildrenList, runCrossAxisExtent); // Per-auto-margin main-axis share. Auto margins in the main axis absorb free space. // https://www.w3.org/TR/css-flexbox-1/#auto-margins - final double perMainAxisAutoMargin = - mainAxisAutoMarginCount == 0 ? 0.0 : (math.max(0, remainingSpace) / mainAxisAutoMarginCount); + final double perMainAxisAutoMargin = mainAxisAutoMarginCount == 0 + ? 0.0 + : (math.max(0, remainingSpace) / mainAxisAutoMarginCount); - final bool mainAxisStartAtPhysicalStart = _isMainAxisStartAtPhysicalStart(); + final bool mainAxisStartAtPhysicalStart = + _isMainAxisStartAtPhysicalStart(); for (_RunChild runChild in runChildrenList) { RenderBox child = runChild.child; @@ -6960,27 +7426,42 @@ class RenderFlexLayout extends RenderLayoutBox { final double childStartMargin = runChild.mainAxisStartMargin; final double childEndMargin = runChild.mainAxisEndMargin; // Border-box main-size of the child (no margins). - final double childMainSizeOnly = _getRunChildMainSize(runChild); + final Size childSize = _getChildSize(child)!; + final double childMainSizeOnly = + isHorizontal ? childSize.width : childSize.height; // Position the child along the main axis respecting direction. final bool mainAxisStartAuto = isHorizontal - ? (mainAxisStartAtPhysicalStart ? runChild.marginLeftAuto : runChild.marginRightAuto) - : (mainAxisStartAtPhysicalStart ? runChild.marginTopAuto : runChild.marginBottomAuto); + ? (mainAxisStartAtPhysicalStart + ? runChild.marginLeftAuto + : runChild.marginRightAuto) + : (mainAxisStartAtPhysicalStart + ? runChild.marginTopAuto + : runChild.marginBottomAuto); final bool mainAxisEndAuto = isHorizontal - ? (mainAxisStartAtPhysicalStart ? runChild.marginRightAuto : runChild.marginLeftAuto) - : (mainAxisStartAtPhysicalStart ? runChild.marginBottomAuto : runChild.marginTopAuto); - final double startAutoSpace = mainAxisStartAuto ? perMainAxisAutoMargin : 0.0; - final double endAutoSpace = mainAxisEndAuto ? perMainAxisAutoMargin : 0.0; + ? (mainAxisStartAtPhysicalStart + ? runChild.marginRightAuto + : runChild.marginLeftAuto) + : (mainAxisStartAtPhysicalStart + ? runChild.marginBottomAuto + : runChild.marginTopAuto); + final double startAutoSpace = + mainAxisStartAuto ? perMainAxisAutoMargin : 0.0; + final double endAutoSpace = + mainAxisEndAuto ? perMainAxisAutoMargin : 0.0; if (flipMainAxis) { // In reversed main axis (e.g., column-reverse or RTL row), advance from the // far edge by the start margin and the child's own size. Do not subtract the // trailing (end) margin here — it separates this item from the next. - final double adjStartMargin = _calculateMainAxisMarginForJustContentType(childStartMargin); - childMainPosition -= (startAutoSpace + adjStartMargin + childMainSizeOnly); + final double adjStartMargin = + _calculateMainAxisMarginForJustContentType(childStartMargin); + childMainPosition -= + (startAutoSpace + adjStartMargin + childMainSizeOnly); } else { // In normal flow, advance by the start margin before placing. - childMainPosition += _calculateMainAxisMarginForJustContentType(childStartMargin); + childMainPosition += + _calculateMainAxisMarginForJustContentType(childStartMargin); childMainPosition += startAutoSpace; } double? childCrossPosition; @@ -6992,11 +7473,13 @@ class RenderFlexLayout extends RenderLayoutBox { case AlignSelf.flexStart: case AlignSelf.start: case AlignSelf.stretch: - alignment = renderStyle.flexWrap == FlexWrap.wrapReverse ? 'end' : 'start'; + alignment = + renderStyle.flexWrap == FlexWrap.wrapReverse ? 'end' : 'start'; break; case AlignSelf.flexEnd: case AlignSelf.end: - alignment = renderStyle.flexWrap == FlexWrap.wrapReverse ? 'start' : 'end'; + alignment = + renderStyle.flexWrap == FlexWrap.wrapReverse ? 'start' : 'end'; break; case AlignSelf.center: alignment = 'center'; @@ -7010,18 +7493,22 @@ class RenderFlexLayout extends RenderLayoutBox { case AlignItems.flexStart: case AlignItems.start: case AlignItems.stretch: - alignment = renderStyle.flexWrap == FlexWrap.wrapReverse ? 'end' : 'start'; + alignment = renderStyle.flexWrap == FlexWrap.wrapReverse + ? 'end' + : 'start'; break; case AlignItems.flexEnd: case AlignItems.end: - alignment = renderStyle.flexWrap == FlexWrap.wrapReverse ? 'start' : 'end'; + alignment = renderStyle.flexWrap == FlexWrap.wrapReverse + ? 'start' + : 'end'; break; case AlignItems.center: alignment = 'center'; break; case AlignItems.baseline: case AlignItems.lastBaseline: - // FIXME: baseline alignment in wrap-reverse flexWrap may display different from browser in some case + // FIXME: baseline alignment in wrap-reverse flexWrap may display different from browser in some case if (isHorizontal) { alignment = 'baseline'; } else if (renderStyle.flexWrap == FlexWrap.wrapReverse) { @@ -7040,7 +7527,9 @@ class RenderFlexLayout extends RenderLayoutBox { // Text is aligned in anonymous block container rather than flexbox container. // https://www.w3.org/TR/css-flexbox-1/#flex-items - if (renderStyle.alignItems == AlignItems.stretch && child is RenderTextBox && !isHorizontal) { + if (renderStyle.alignItems == AlignItems.stretch && + child is RenderTextBox && + !isHorizontal) { TextAlign textAlign = renderStyle.textAlign; if (textAlign == TextAlign.start) { alignment = 'start'; @@ -7051,7 +7540,9 @@ class RenderFlexLayout extends RenderLayoutBox { } } - final double childCrossAxisExtent = _getRunChildCrossAxisExtent(runChild); + final double childCrossAxisExtent = + (isHorizontal ? childSize.height : childSize.width) + + runChild.crossAxisExtentAdjustment; childCrossPosition = _getChildCrossAxisOffset( alignment, child, @@ -7072,24 +7563,29 @@ class RenderFlexLayout extends RenderLayoutBox { // https://www.w3.org/TR/css-flexbox-1/#auto-margins if (child is RenderBoxModel) { // Margin auto does not work with negative remaining space. - final double crossAxisRemainingSpace = math.max(0, crossAxisContentSize - childCrossAxisExtent); + final double crossAxisRemainingSpace = + math.max(0, crossAxisContentSize - childCrossAxisExtent); if (isHorizontal) { // Cross axis is vertical (top/bottom). if (runChild.marginTopAuto) { if (runChild.marginBottomAuto) { - childCrossPosition = childCrossPosition! + crossAxisRemainingSpace / 2; + childCrossPosition = + childCrossPosition! + crossAxisRemainingSpace / 2; } else { - childCrossPosition = childCrossPosition! + crossAxisRemainingSpace; + childCrossPosition = + childCrossPosition! + crossAxisRemainingSpace; } } } else { // Cross axis is horizontal (left/right). if (runChild.marginLeftAuto) { if (runChild.marginRightAuto) { - childCrossPosition = childCrossPosition! + crossAxisRemainingSpace / 2; + childCrossPosition = + childCrossPosition! + crossAxisRemainingSpace / 2; } else { - childCrossPosition = childCrossPosition! + crossAxisRemainingSpace; + childCrossPosition = + childCrossPosition! + crossAxisRemainingSpace; } } } @@ -7099,8 +7595,11 @@ class RenderFlexLayout extends RenderLayoutBox { double crossOffset; if (renderStyle.flexWrap == FlexWrap.wrapReverse) { - crossOffset = - childCrossPosition! + (crossAxisContentSize - crossAxisOffset - runCrossAxisExtent - runBetweenSpace); + crossOffset = childCrossPosition! + + (crossAxisContentSize - + crossAxisOffset - + runCrossAxisExtent - + runBetweenSpace); } else { crossOffset = childCrossPosition! + crossAxisOffset; } @@ -7117,10 +7616,15 @@ class RenderFlexLayout extends RenderLayoutBox { if (flipMainAxis) { // After placing in reversed flow, move past the trailing (end) margin, // then account for between-space and gaps. - childMainPosition -= (childEndMargin + endAutoSpace + betweenSpace + effectiveGap); + childMainPosition -= + (childEndMargin + endAutoSpace + betweenSpace + effectiveGap); } else { // Normal flow: advance by the child size, trailing margin, between-space and gaps. - childMainPosition += (childMainSizeOnly + childEndMargin + endAutoSpace + betweenSpace + effectiveGap); + childMainPosition += (childMainSizeOnly + + childEndMargin + + endAutoSpace + + betweenSpace + + effectiveGap); } } @@ -7183,14 +7687,16 @@ class RenderFlexLayout extends RenderLayoutBox { // Replaced elements (e.g., ) with an intrinsic aspect ratio should not be // stretched in the cross axis; browsers keep their border-box proportional even // under align-items: stretch. This matches CSS Flexbox §9.4. - if (_shouldPreserveIntrinsicRatio(childBoxModel, hasDefiniteContainerCross: hasDefiniteCrossSize)) { + if (_shouldPreserveIntrinsicRatio(childBoxModel, + hasDefiniteContainerCross: hasDefiniteCrossSize)) { return false; } return true; } - bool _shouldPreserveIntrinsicRatio(RenderBoxModel child, {required bool hasDefiniteContainerCross}) { + bool _shouldPreserveIntrinsicRatio(RenderBoxModel child, + {required bool hasDefiniteContainerCross}) { if (child is! RenderReplaced) { return false; } @@ -7209,12 +7715,14 @@ class RenderFlexLayout extends RenderLayoutBox { bool _hasDefiniteContainerCrossSize() { if (_isHorizontalFlexDirection) { if (renderStyle.contentBoxLogicalHeight != null) return true; - if (contentConstraints != null && contentConstraints!.hasTightHeight) return true; + if (contentConstraints != null && contentConstraints!.hasTightHeight) + return true; if (constraints.hasTightHeight) return true; return false; } else { if (renderStyle.contentBoxLogicalWidth != null) return true; - if (contentConstraints != null && contentConstraints!.hasTightWidth) return true; + if (contentConstraints != null && contentConstraints!.hasTightWidth) + return true; if (constraints.hasTightWidth) return true; final bool canResolveAutoWidthFromBounds = renderStyle.writingMode == CSSWritingMode.horizontalTb && @@ -7230,45 +7738,64 @@ class RenderFlexLayout extends RenderLayoutBox { } // Get child stretched size in the cross axis. - double _getChildStretchedCrossSize(RenderBoxModel child, - double runCrossAxisExtent, - double runBetweenSpace,) { - bool isFlexWrap = renderStyle.flexWrap == FlexWrap.wrap || renderStyle.flexWrap == FlexWrap.wrapReverse; - double childCrossAxisMargin = _horizontalMarginNegativeSet(0, child, isHorizontal: !_isHorizontalFlexDirection); - _isHorizontalFlexDirection ? child.renderStyle.margin.vertical : child.renderStyle.margin.horizontal; - double maxCrossSizeConstraints = _isHorizontalFlexDirection ? constraints.maxHeight : constraints.maxWidth; - double flexLineCrossSize = _getFlexLineCrossSize(child, runCrossAxisExtent, runBetweenSpace); + double _getChildStretchedCrossSize( + RenderBoxModel child, + double runCrossAxisExtent, + double runBetweenSpace, + ) { + bool isFlexWrap = renderStyle.flexWrap == FlexWrap.wrap || + renderStyle.flexWrap == FlexWrap.wrapReverse; + double childCrossAxisMargin = _horizontalMarginNegativeSet(0, child, + isHorizontal: !_isHorizontalFlexDirection); + _isHorizontalFlexDirection + ? child.renderStyle.margin.vertical + : child.renderStyle.margin.horizontal; + double maxCrossSizeConstraints = _isHorizontalFlexDirection + ? constraints.maxHeight + : constraints.maxWidth; + double flexLineCrossSize = + _getFlexLineCrossSize(child, runCrossAxisExtent, runBetweenSpace); // Should subtract margin when stretch flex item. double childStretchedCrossSize = flexLineCrossSize - childCrossAxisMargin; // Flex line cross size should not exceed container's cross size if specified when flex-wrap is nowrap. if (!isFlexWrap && maxCrossSizeConstraints.isFinite) { - double crossAxisBorder = _isHorizontalFlexDirection ? renderStyle.border.vertical : renderStyle.border.horizontal; - double crossAxisPadding = - _isHorizontalFlexDirection ? renderStyle.padding.vertical : renderStyle.padding.horizontal; - childStretchedCrossSize = - math.min(maxCrossSizeConstraints - crossAxisBorder - crossAxisPadding, childStretchedCrossSize); + double crossAxisBorder = _isHorizontalFlexDirection + ? renderStyle.border.vertical + : renderStyle.border.horizontal; + double crossAxisPadding = _isHorizontalFlexDirection + ? renderStyle.padding.vertical + : renderStyle.padding.horizontal; + childStretchedCrossSize = math.min( + maxCrossSizeConstraints - crossAxisBorder - crossAxisPadding, + childStretchedCrossSize); } // Constrain stretched size by max-width/max-height. double? maxCrossSize; if (_isHorizontalFlexDirection && child.renderStyle.maxHeight.isNotNone) { maxCrossSize = child.renderStyle.maxHeight.computedValue; - } else if (!_isHorizontalFlexDirection && child.renderStyle.maxWidth.isNotNone) { + } else if (!_isHorizontalFlexDirection && + child.renderStyle.maxWidth.isNotNone) { maxCrossSize = child.renderStyle.maxWidth.computedValue; } if (maxCrossSize != null) { - childStretchedCrossSize = childStretchedCrossSize > maxCrossSize ? maxCrossSize : childStretchedCrossSize; + childStretchedCrossSize = childStretchedCrossSize > maxCrossSize + ? maxCrossSize + : childStretchedCrossSize; } // Constrain stretched size by min-width/min-height. double? minCrossSize; if (_isHorizontalFlexDirection && child.renderStyle.minHeight.isNotAuto) { minCrossSize = child.renderStyle.minHeight.computedValue; - } else if (!_isHorizontalFlexDirection && child.renderStyle.minWidth.isNotAuto) { + } else if (!_isHorizontalFlexDirection && + child.renderStyle.minWidth.isNotAuto) { minCrossSize = child.renderStyle.minWidth.computedValue; } if (minCrossSize != null) { - childStretchedCrossSize = childStretchedCrossSize < minCrossSize ? minCrossSize : childStretchedCrossSize; + childStretchedCrossSize = childStretchedCrossSize < minCrossSize + ? minCrossSize + : childStretchedCrossSize; } // Ensure stretched height in row-direction is not smaller than the @@ -7306,27 +7833,30 @@ class RenderFlexLayout extends RenderLayoutBox { final CSSLengthValue marginRight = s.marginRight; final CSSLengthValue marginTop = s.marginTop; final CSSLengthValue marginBottom = s.marginBottom; - if (_isHorizontalFlexDirection && (marginTop.isAuto || marginBottom.isAuto)) return true; - if (!_isHorizontalFlexDirection && (marginLeft.isAuto || marginRight.isAuto)) return true; + if (_isHorizontalFlexDirection && + (marginTop.isAuto || marginBottom.isAuto)) return true; + if (!_isHorizontalFlexDirection && + (marginLeft.isAuto || marginRight.isAuto)) return true; } return false; } // Get flex item cross axis offset by align-items/align-self. - double? _getChildCrossAxisOffset(String alignment, - RenderBox child, - double? childCrossPosition, - double runBaselineExtent, - double runCrossAxisExtent, - double runBetweenSpace, - double crossAxisStartPadding, - double crossAxisStartBorder, { - required double childCrossAxisExtent, - required double childCrossAxisStartMargin, - required double childCrossAxisEndMargin, - required bool hasAutoCrossAxisMargin, - required bool runAllChildrenAtMaxCross, - }) { + double? _getChildCrossAxisOffset( + String alignment, + RenderBox child, + double? childCrossPosition, + double runBaselineExtent, + double runCrossAxisExtent, + double runBetweenSpace, + double crossAxisStartPadding, + double crossAxisStartBorder, { + required double childCrossAxisExtent, + required double childCrossAxisStartMargin, + required double childCrossAxisEndMargin, + required bool hasAutoCrossAxisMargin, + required bool runAllChildrenAtMaxCross, + }) { // Leading between height of line box's content area and line height of line box. double lineBoxLeading = 0; double? lineBoxHeight = _getLineHeight(this); @@ -7340,13 +7870,16 @@ class RenderFlexLayout extends RenderLayoutBox { runBetweenSpace, ); // start offset including margin (used by start/end alignment) - double crossStartAddedOffset = crossAxisStartPadding + crossAxisStartBorder + childCrossAxisStartMargin; + double crossStartAddedOffset = crossAxisStartPadding + + crossAxisStartBorder + + childCrossAxisStartMargin; // start offset without margin (used by center alignment where we center the margin-box itself) double crossStartNoMargin = crossAxisStartPadding + crossAxisStartBorder; final _FlexContainerInvariants? inv = _layoutInvariants; final bool crossIsHorizontal; - final bool crossStartIsPhysicalStart; // left for horizontal, top for vertical + final bool + crossStartIsPhysicalStart; // left for horizontal, top for vertical if (inv != null) { crossIsHorizontal = inv.isCrossAxisHorizontal; crossStartIsPhysicalStart = inv.isCrossAxisStartAtPhysicalStart; @@ -7354,7 +7887,8 @@ class RenderFlexLayout extends RenderLayoutBox { // Determine cross axis orientation and where cross-start maps physically. final CSSWritingMode wm = renderStyle.writingMode; final bool inlineIsHorizontal = (wm == CSSWritingMode.horizontalTb); - if (renderStyle.flexDirection == FlexDirection.row || renderStyle.flexDirection == FlexDirection.rowReverse) { + if (renderStyle.flexDirection == FlexDirection.row || + renderStyle.flexDirection == FlexDirection.rowReverse) { // Cross is block axis. crossIsHorizontal = !inlineIsHorizontal; if (crossIsHorizontal) { @@ -7369,7 +7903,8 @@ class RenderFlexLayout extends RenderLayoutBox { crossIsHorizontal = inlineIsHorizontal; if (crossIsHorizontal) { // Inline-start follows text direction in horizontal-tb. - crossStartIsPhysicalStart = (renderStyle.direction != TextDirection.rtl); + crossStartIsPhysicalStart = + (renderStyle.direction != TextDirection.rtl); } else { // Inline-start is physical top in vertical writing modes. crossStartIsPhysicalStart = true; @@ -7404,16 +7939,20 @@ class RenderFlexLayout extends RenderLayoutBox { childCrossAxisStartMargin; } else { // Cross-start at right: cross-end is left (physical start) - return crossAxisStartPadding + crossAxisStartBorder + childCrossAxisEndMargin; + return crossAxisStartPadding + + crossAxisStartBorder + + childCrossAxisEndMargin; } case 'center': // Center the child's MARGIN-BOX within the flex line's content box (spec behavior). // We first get the child's cross-extent including margins, then derive the // border-box extent for overflow heuristics and logging. - final double childExtentWithMargin = childCrossAxisExtent; // includes margins + final double childExtentWithMargin = + childCrossAxisExtent; // includes margins final double startMargin = childCrossAxisStartMargin; final double endMargin = childCrossAxisEndMargin; - final double borderBoxExtent = math.max(0.0, childExtentWithMargin - (startMargin + endMargin)); + final double borderBoxExtent = + math.max(0.0, childExtentWithMargin - (startMargin + endMargin)); // Center within the content box by default (spec-aligned). // Additionally, for vertical cross-axes (row direction), when the item overflows // the content box (free space < 0) and the container has cross-axis padding, @@ -7431,26 +7970,38 @@ class RenderFlexLayout extends RenderLayoutBox { // centering of padded controls. This preserves expected behavior for // headers/toolbars where padding defines visual bounds. final double padStart = crossAxisStartPadding; - final double padEnd = inv?.crossAxisPaddingEnd ?? _flowAwareCrossAxisPadding(isEnd: true); + final double padEnd = inv?.crossAxisPaddingEnd ?? + _flowAwareCrossAxisPadding(isEnd: true); final double borderStart = crossAxisStartBorder; - final double borderEnd = inv?.crossAxisBorderEnd ?? renderStyle.effectiveBorderBottomWidth.computedValue; - final double padBorderSum = padStart + padEnd + borderStart + borderEnd; + final double borderEnd = inv?.crossAxisBorderEnd ?? + renderStyle.effectiveBorderBottomWidth.computedValue; + final double padBorderSum = + padStart + padEnd + borderStart + borderEnd; final double freeSpace = flexLineCrossSize - borderBoxExtent; const double kEpsilon = 0.0001; final bool isExactFit = freeSpace.abs() <= kEpsilon; - if (padBorderSum > 0 && (freeSpace < 0 || (isExactFit && runAllChildrenAtMaxCross))) { + if (padBorderSum > 0 && + (freeSpace < 0 || (isExactFit && runAllChildrenAtMaxCross))) { // Determine container content cross size (definite if set on style), // fall back to the current line cross size. - final double containerContentCross = renderStyle.contentBoxLogicalHeight ?? flexLineCrossSize; - final double containerBorderCross = containerContentCross + padStart + padEnd + borderStart + borderEnd; - final double posFromBorder = (containerBorderCross - borderBoxExtent) / 2.0; - final double pos = posFromBorder; // since offsets are measured from border-start + final double containerContentCross = + renderStyle.contentBoxLogicalHeight ?? flexLineCrossSize; + final double containerBorderCross = containerContentCross + + padStart + + padEnd + + borderStart + + borderEnd; + final double posFromBorder = + (containerBorderCross - borderBoxExtent) / 2.0; + final double pos = + posFromBorder; // since offsets are measured from border-start return pos.isFinite ? pos : crossStartNoMargin; } } // If the margin-box is equal to or wider than the line cross size, pin to start // to avoid introducing cross-axis offset that would create horizontal scroll. - final double marginBoxExtent = borderBoxExtent + startMargin + endMargin; + final double marginBoxExtent = + borderBoxExtent + startMargin + endMargin; // Only clamp for horizontal cross-axis (i.e., when cross is width), and only when // overflow is caused by margins (border-box fits, margin-box overflows). If the // border-box itself is wider than the line, we still center (allow negative offset) @@ -7458,28 +8009,33 @@ class RenderFlexLayout extends RenderLayoutBox { // Only treat as overflow when the margin-box actually exceeds the line cross size. // If it exactly equals, there is no overflow and centering should place the // border-box at startMargin (i.e., symmetric gaps), matching browser behavior. - final bool marginOnlyOverflow = borderBoxExtent <= flexLineCrossSize && marginBoxExtent > flexLineCrossSize; + final bool marginOnlyOverflow = borderBoxExtent <= flexLineCrossSize && + marginBoxExtent > flexLineCrossSize; if (crossIsHorizontal && marginOnlyOverflow) { return crossStartNoMargin; } // Center the margin-box in the line's content box, then add the start margin // to obtain the border-box offset. final double freeInContent = flexLineCrossSize - marginBoxExtent; - final double pos = crossStartNoMargin + freeInContent / 2.0 + startMargin; + final double pos = + crossStartNoMargin + freeInContent / 2.0 + startMargin; return pos; case 'baseline': - // In column flex-direction (vertical main axis), baseline alignment behaves - // like flex-start per our layout model. Avoid using runBaselineExtent which - // is not computed for vertical-main containers. + // In column flex-direction (vertical main axis), baseline alignment behaves + // like flex-start per our layout model. Avoid using runBaselineExtent which + // is not computed for vertical-main containers. if (!_isHorizontalFlexDirection) { return crossStartAddedOffset; } // Distance from top to baseline of child. double childAscent = _getChildAscent(child); - final double offset = crossStartAddedOffset + lineBoxLeading / 2 + (runBaselineExtent - childAscent); + final double offset = crossStartAddedOffset + + lineBoxLeading / 2 + + (runBaselineExtent - childAscent); if (DebugFlags.debugLogFlexBaselineEnabled) { // ignore: avoid_print - print('[FlexBaseline] offset child=${child.runtimeType}#${child.hashCode} ' + print( + '[FlexBaseline] offset child=${child.runtimeType}#${child.hashCode} ' 'runBaseline=${runBaselineExtent.toStringAsFixed(2)} ' 'childAscent=${childAscent.toStringAsFixed(2)} ' 'lineLeading=${lineBoxLeading.toStringAsFixed(2)} ' @@ -7492,10 +8048,14 @@ class RenderFlexLayout extends RenderLayoutBox { } } - bool _areAllRunChildrenAtMaxCrossExtent(List<_RunChild> runChildren, double runCrossAxisExtent) { + bool _areAllRunChildrenAtMaxCrossExtent( + List<_RunChild> runChildren, double runCrossAxisExtent) { const double kEpsilon = 0.0001; for (final _RunChild runChild in runChildren) { - final double childCrossAxisExtent = _getRunChildCrossAxisExtent(runChild); + final Size childSize = _getChildSize(runChild.child)!; + final double childCrossAxisExtent = + (_isHorizontalFlexDirection ? childSize.height : childSize.width) + + runChild.crossAxisExtentAdjustment; if ((childCrossAxisExtent - runCrossAxisExtent).abs() > kEpsilon) { return false; } @@ -7542,9 +8102,11 @@ class RenderFlexLayout extends RenderLayoutBox { } final double? desiredPreservedMain = _childrenIntrinsicMainSizes[child]; + final Size childSize = _getChildSize(child)!; + final double childMainSize = + isHorizontal ? childSize.width : childSize.height; if (desiredPreservedMain != null && - (desiredPreservedMain - _getRunChildMainSize(runChild)).abs() >= - 0.5) { + (desiredPreservedMain - childMainSize).abs() >= 0.5) { return false; } @@ -7571,7 +8133,8 @@ class RenderFlexLayout extends RenderLayoutBox { } // Get child size through boxSize to avoid flutter error when parentUsesSize is set to false. - Size? _getChildSize(RenderBox? child, {bool shouldUseIntrinsicMainSize = false}) { + Size? _getChildSize(RenderBox? child, + {bool shouldUseIntrinsicMainSize = false}) { Size? childSize; if (child != null) { childSize = _transientChildSizeOverrides?[child]; @@ -7692,10 +8255,14 @@ class RenderFlexLayout extends RenderLayoutBox { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('flexDirection', renderStyle.flexDirection)); - properties.add(DiagnosticsProperty('justifyContent', renderStyle.justifyContent)); - properties.add(DiagnosticsProperty('alignItems', renderStyle.alignItems)); - properties.add(DiagnosticsProperty('flexWrap', renderStyle.flexWrap)); + properties.add(DiagnosticsProperty( + 'flexDirection', renderStyle.flexDirection)); + properties.add(DiagnosticsProperty( + 'justifyContent', renderStyle.justifyContent)); + properties.add( + DiagnosticsProperty('alignItems', renderStyle.alignItems)); + properties + .add(DiagnosticsProperty('flexWrap', renderStyle.flexWrap)); } static bool _isPlaceholderPositioned(RenderObject child) { From c6b0d405199007a589546338b25857e5c3280464 Mon Sep 17 00:00:00 2001 From: andycall Date: Sun, 29 Mar 2026 00:40:53 -0700 Subject: [PATCH 12/13] fix(webf): avoid debug-only layout reuse checks --- webf/lib/src/foundation/debug_flags.dart | 51 +- webf/lib/src/rendering/box_model.dart | 31 + webf/lib/src/rendering/box_wrapper.dart | 16 +- webf/lib/src/rendering/event_listener.dart | 16 +- webf/lib/src/rendering/flex.dart | 984 ++++++++++++++++++++- webf/lib/src/rendering/flow.dart | 16 +- 6 files changed, 1038 insertions(+), 76 deletions(-) diff --git a/webf/lib/src/foundation/debug_flags.dart b/webf/lib/src/foundation/debug_flags.dart index 280b893ec3..f93bb51ee2 100644 --- a/webf/lib/src/foundation/debug_flags.dart +++ b/webf/lib/src/foundation/debug_flags.dart @@ -117,26 +117,47 @@ class DebugFlags { static int cssGridProfilingMinMs = 2; // Removed: Use FlexLog filters to enable flex logs. - static bool enableFlexFastPathProfiling = - const bool.fromEnvironment('WEBF_DEBUG_FLEX_FAST_PATH', defaultValue: false); - static int flexFastPathProfilingSummaryEvery = - const int.fromEnvironment('WEBF_DEBUG_FLEX_FAST_PATH_SUMMARY_EVERY', defaultValue: 50); - static int flexFastPathProfilingMaxDetailLogs = - const int.fromEnvironment('WEBF_DEBUG_FLEX_FAST_PATH_MAX_DETAIL_LOGS', defaultValue: 20); - static bool enableFlexAdjustFastPathProfiling = - const bool.fromEnvironment('WEBF_DEBUG_FLEX_ADJUST_FAST_PATH', defaultValue: false); + static bool enableFlexFastPathProfiling = const bool.fromEnvironment( + 'WEBF_DEBUG_FLEX_FAST_PATH', + defaultValue: false); + static int flexFastPathProfilingSummaryEvery = const int.fromEnvironment( + 'WEBF_DEBUG_FLEX_FAST_PATH_SUMMARY_EVERY', + defaultValue: 50); + static int flexFastPathProfilingMaxDetailLogs = const int.fromEnvironment( + 'WEBF_DEBUG_FLEX_FAST_PATH_MAX_DETAIL_LOGS', + defaultValue: 20); + static bool enableFlexAdjustFastPathProfiling = const bool.fromEnvironment( + 'WEBF_DEBUG_FLEX_ADJUST_FAST_PATH', + defaultValue: false); static int flexAdjustFastPathProfilingSummaryEvery = - const int.fromEnvironment('WEBF_DEBUG_FLEX_ADJUST_FAST_PATH_SUMMARY_EVERY', defaultValue: 50); + const int.fromEnvironment( + 'WEBF_DEBUG_FLEX_ADJUST_FAST_PATH_SUMMARY_EVERY', + defaultValue: 50); static int flexAdjustFastPathProfilingMaxDetailLogs = - const int.fromEnvironment('WEBF_DEBUG_FLEX_ADJUST_FAST_PATH_MAX_DETAIL_LOGS', defaultValue: 20); - static bool enableFlexAnonymousMetricsProfiling = - const bool.fromEnvironment('WEBF_DEBUG_FLEX_ANON_METRICS', defaultValue: false); + const int.fromEnvironment( + 'WEBF_DEBUG_FLEX_ADJUST_FAST_PATH_MAX_DETAIL_LOGS', + defaultValue: 20); + static bool enableFlexAnonymousMetricsProfiling = const bool.fromEnvironment( + 'WEBF_DEBUG_FLEX_ANON_METRICS', + defaultValue: false); static int flexAnonymousMetricsProfilingSummaryEvery = - const int.fromEnvironment('WEBF_DEBUG_FLEX_ANON_METRICS_SUMMARY_EVERY', defaultValue: 50); + const int.fromEnvironment('WEBF_DEBUG_FLEX_ANON_METRICS_SUMMARY_EVERY', + defaultValue: 50); static int flexAnonymousMetricsProfilingMaxDetailLogs = - const int.fromEnvironment('WEBF_DEBUG_FLEX_ANON_METRICS_MAX_DETAIL_LOGS', defaultValue: 20); + const int.fromEnvironment('WEBF_DEBUG_FLEX_ANON_METRICS_MAX_DETAIL_LOGS', + defaultValue: 20); static String flexAnonymousMetricsProfilingWatchedPathContains = - const String.fromEnvironment('WEBF_DEBUG_FLEX_ANON_METRICS_WATCH_PATH', defaultValue: ''); + const String.fromEnvironment('WEBF_DEBUG_FLEX_ANON_METRICS_WATCH_PATH', + defaultValue: ''); + static bool enableFlexLayoutChildProfiling = const bool.fromEnvironment( + 'WEBF_DEBUG_FLEX_LAYOUT_CHILD', + defaultValue: false); + static int flexLayoutChildProfilingSummaryEvery = const int.fromEnvironment( + 'WEBF_DEBUG_FLEX_LAYOUT_CHILD_SUMMARY_EVERY', + defaultValue: 200); + static int flexLayoutChildProfilingMaxDetailLogs = const int.fromEnvironment( + 'WEBF_DEBUG_FLEX_LAYOUT_CHILD_MAX_DETAIL_LOGS', + defaultValue: 20); /// Debug flag to enable inline layout visualization. /// When true, paints debug information for line boxes, margins, padding, etc. diff --git a/webf/lib/src/rendering/box_model.dart b/webf/lib/src/rendering/box_model.dart index 31db46aabc..646110bc52 100644 --- a/webf/lib/src/rendering/box_model.dart +++ b/webf/lib/src/rendering/box_model.dart @@ -111,6 +111,37 @@ Offset getLayoutTransformTo(RenderObject current, RenderObject ancestor, return stackOffsets.reduce((prev, next) => prev + next); } +bool debugRenderObjectNeedsLayout(RenderObject renderObject) { + bool result = false; + assert(() { + result = renderObject.debugNeedsLayout; + return true; + }()); + return result; +} + +bool canReuseStableProxyChildLayout(RenderBox? child, BoxConstraints constraints) { + if (child == null || !child.hasSize || child.constraints != constraints) { + return false; + } + return _hasReusableStableLayoutChain(child); +} + +bool _hasReusableStableLayoutChain(RenderBox child) { + if (child is RenderTextBox) { + return !child.hasPendingTextLayoutUpdate; + } + if (child is RenderBoxModel) { + return !child.needsRelayout && + !child.hasPendingSubtreeIntrinsicMeasurementInvalidation; + } + if (child is RenderProxyBox) { + final RenderBox? proxyChild = child.child; + return proxyChild == null || _hasReusableStableLayoutChain(proxyChild); + } + return false; +} + mixin RenderBoxModelBase on RenderBox { CSSRenderStyle get renderStyle; } diff --git a/webf/lib/src/rendering/box_wrapper.dart b/webf/lib/src/rendering/box_wrapper.dart index 6dd5fdf7bf..2ac95ed2a2 100644 --- a/webf/lib/src/rendering/box_wrapper.dart +++ b/webf/lib/src/rendering/box_wrapper.dart @@ -14,21 +14,7 @@ import 'package:webf/rendering.dart'; import 'package:webf/dom.dart' as dom; bool _canReuseWrappedChildLayout(RenderBox? child, BoxConstraints constraints) { - if (child == null || !child.hasSize || child.constraints != constraints) { - return false; - } - if (child.debugNeedsLayout) { - return false; - } - if (child is RenderTextBox && child.hasPendingTextLayoutUpdate) { - return false; - } - if (child is RenderBoxModel && - (child.needsRelayout || - child.hasPendingSubtreeIntrinsicMeasurementInvalidation)) { - return false; - } - return true; + return canReuseStableProxyChildLayout(child, constraints); } class RenderLayoutBoxWrapper extends RenderBoxModel diff --git a/webf/lib/src/rendering/event_listener.dart b/webf/lib/src/rendering/event_listener.dart index 07d9ce30b6..5e4f0890d4 100644 --- a/webf/lib/src/rendering/event_listener.dart +++ b/webf/lib/src/rendering/event_listener.dart @@ -15,21 +15,7 @@ import 'package:webf/gesture.dart'; import 'package:webf/rendering.dart' hide RenderBoxContainerDefaultsMixin; bool _canReuseProxyChildLayout(RenderBox? child, BoxConstraints constraints) { - if (child == null || !child.hasSize || child.constraints != constraints) { - return false; - } - if (child.debugNeedsLayout) { - return false; - } - if (child is RenderTextBox && child.hasPendingTextLayoutUpdate) { - return false; - } - if (child is RenderBoxModel && - (child.needsRelayout || - child.hasPendingSubtreeIntrinsicMeasurementInvalidation)) { - return false; - } - return true; + return canReuseStableProxyChildLayout(child, constraints); } class RenderPortalsParentData extends RenderLayoutParentData {} diff --git a/webf/lib/src/rendering/flex.dart b/webf/lib/src/rendering/flex.dart index ebe84039c3..534d9c1337 100644 --- a/webf/lib/src/rendering/flex.dart +++ b/webf/lib/src/rendering/flex.dart @@ -178,6 +178,227 @@ String _flexAdjustFastPathRelayoutReasonLabel( } } +enum _FlexLayoutChildReason { + computeRunMetrics, + singleFlexibleRun, + adjustPhase1, + adjustStretch, + positionPlaceholder, +} + +String _flexLayoutChildReasonLabel(_FlexLayoutChildReason reason) { + switch (reason) { + case _FlexLayoutChildReason.computeRunMetrics: + return 'computeRunMetrics'; + case _FlexLayoutChildReason.singleFlexibleRun: + return 'singleFlexibleRun'; + case _FlexLayoutChildReason.adjustPhase1: + return 'adjustPhase1'; + case _FlexLayoutChildReason.adjustStretch: + return 'adjustStretch'; + case _FlexLayoutChildReason.positionPlaceholder: + return 'positionPlaceholder'; + } +} + +enum _FlexMeasurementReuseMissReason { + missingSize, + constraintsMismatch, + childNeedsLayout, + effectiveChildNeedsRelayout, + postMeasureLayout, + pendingIntrinsicInvalidation, + other, +} + +String _flexMeasurementReuseMissReasonLabel( + _FlexMeasurementReuseMissReason reason, +) { + switch (reason) { + case _FlexMeasurementReuseMissReason.missingSize: + return 'missingSize'; + case _FlexMeasurementReuseMissReason.constraintsMismatch: + return 'constraintsMismatch'; + case _FlexMeasurementReuseMissReason.childNeedsLayout: + return 'childNeedsLayout'; + case _FlexMeasurementReuseMissReason.effectiveChildNeedsRelayout: + return 'effectiveChildNeedsRelayout'; + case _FlexMeasurementReuseMissReason.postMeasureLayout: + return 'postMeasureLayout'; + case _FlexMeasurementReuseMissReason.pendingIntrinsicInvalidation: + return 'pendingIntrinsicInvalidation'; + case _FlexMeasurementReuseMissReason.other: + return 'other'; + } +} + +String _describeFlexLayoutChildKind(RenderBox child) { + final List parts = []; + RenderBox? current = child; + int depth = 0; + while (current != null && depth < 4) { + if (current is RenderEventListener) { + parts.add('event'); + current = current.child; + } else if (current is RenderLayoutBoxWrapper) { + parts.add('wrapper'); + current = current.child; + } else if (current is RenderFlexLayout) { + parts.add( + current._isHorizontalFlexDirection + ? (current.renderStyle.flexWrap == FlexWrap.nowrap + ? 'flex-row-nowrap' + : 'flex-row-wrap') + : (current.renderStyle.flexWrap == FlexWrap.nowrap + ? 'flex-col-nowrap' + : 'flex-col-wrap'), + ); + break; + } else if (current is RenderFlowLayout) { + final CSSDisplay display = current.renderStyle.effectiveDisplay; + parts.add('flow:$display'); + break; + } else if (current is RenderTextBox) { + parts.add('text'); + break; + } else if (current is RenderBoxModel) { + parts.add(current.runtimeType.toString()); + break; + } else { + parts.add(current.runtimeType.toString()); + break; + } + depth++; + } + return parts.join('>'); +} + +String _formatProfilerTop( + Map counts, + String Function(T value) labelFor, +) { + if (counts.isEmpty) return 'none'; + final List> entries = counts.entries.toList() + ..sort( + (MapEntry a, MapEntry b) => b.value.compareTo(a.value)); + return entries + .take(5) + .map((MapEntry entry) => '${labelFor(entry.key)}=${entry.value}') + .join(', '); +} + +class _FlexLayoutChildProfiler { + static int _layouts = 0; + static int _detailLogs = 0; + static final Map<_FlexLayoutChildReason, int> _reasonCounts = + <_FlexLayoutChildReason, int>{}; + static final Map _childKindCounts = {}; + static final Map _reasonChildCounts = {}; + + static bool get enabled => DebugFlags.enableFlexLayoutChildProfiling; + + static int get _summaryEvery { + final int configured = DebugFlags.flexLayoutChildProfilingSummaryEvery; + return configured > 0 ? configured : 200; + } + + static int get _maxDetailLogs { + final int configured = DebugFlags.flexLayoutChildProfilingMaxDetailLogs; + return configured >= 0 ? configured : 0; + } + + static void record( + _FlexLayoutChildReason reason, + RenderBox child, + BoxConstraints constraints, + ) { + if (!enabled) return; + _layouts++; + _reasonCounts.update(reason, (int value) => value + 1, ifAbsent: () => 1); + final String childKind = _describeFlexLayoutChildKind(child); + _childKindCounts.update(childKind, (int value) => value + 1, + ifAbsent: () => 1); + final String reasonChildKey = + '${_flexLayoutChildReasonLabel(reason)}:$childKind'; + _reasonChildCounts.update(reasonChildKey, (int value) => value + 1, + ifAbsent: () => 1); + + if (_detailLogs < _maxDetailLogs) { + renderingLogger.info( + '[FlexLayoutChild][layout] reason=${_flexLayoutChildReasonLabel(reason)} ' + 'child=$childKind constraints=$constraints', + ); + _detailLogs++; + } + + if (_layouts % _summaryEvery == 0) { + renderingLogger.info( + '[FlexLayoutChild][summary] layouts=$_layouts ' + 'reasons=${_formatProfilerTop(_reasonCounts, (v) => _flexLayoutChildReasonLabel(v))} ' + 'children=${_formatProfilerTop(_childKindCounts, (v) => v)} ' + 'reasonChildren=${_formatProfilerTop(_reasonChildCounts, (v) => v)}', + ); + } + } +} + +class _FlexMeasurementReuseProfiler { + static int _misses = 0; + static final Map<_FlexMeasurementReuseMissReason, int> _reasonCounts = + <_FlexMeasurementReuseMissReason, int>{}; + static final Map _reasonChildCounts = {}; + + static bool get enabled => DebugFlags.enableFlexLayoutChildProfiling; + + static int get _summaryEvery { + final int configured = DebugFlags.flexLayoutChildProfilingSummaryEvery; + return configured > 0 ? configured : 200; + } + + static void recordMiss( + _FlexLayoutChildReason caller, + RenderBox child, + BoxConstraints constraints, { + RenderBoxModel? effectiveChild, + required bool requiresPostMeasureLayout, + required bool subtreeHasPendingIntrinsicInvalidation, + }) { + if (!enabled) return; + final _FlexMeasurementReuseMissReason reason; + if (!child.hasSize) { + reason = _FlexMeasurementReuseMissReason.missingSize; + } else if (child.constraints != constraints) { + reason = _FlexMeasurementReuseMissReason.constraintsMismatch; + } else if (debugRenderObjectNeedsLayout(child)) { + reason = _FlexMeasurementReuseMissReason.childNeedsLayout; + } else if (effectiveChild != null && effectiveChild.needsRelayout) { + reason = _FlexMeasurementReuseMissReason.effectiveChildNeedsRelayout; + } else if (requiresPostMeasureLayout) { + reason = _FlexMeasurementReuseMissReason.postMeasureLayout; + } else if (subtreeHasPendingIntrinsicInvalidation) { + reason = _FlexMeasurementReuseMissReason.pendingIntrinsicInvalidation; + } else { + reason = _FlexMeasurementReuseMissReason.other; + } + + _misses++; + _reasonCounts.update(reason, (int value) => value + 1, ifAbsent: () => 1); + final String childKind = _describeFlexLayoutChildKind(child); + final String reasonChildKey = + '${_flexLayoutChildReasonLabel(caller)}:${_flexMeasurementReuseMissReasonLabel(reason)}:$childKind'; + _reasonChildCounts.update(reasonChildKey, (int value) => value + 1, + ifAbsent: () => 1); + + if (_misses % _summaryEvery == 0) { + renderingLogger.info( + '[FlexMeasurementReuse][summary] misses=$_misses ' + 'reasons=${_formatProfilerTop(_reasonCounts, (v) => _flexMeasurementReuseMissReasonLabel(v))} ' + 'reasonChildren=${_formatProfilerTop(_reasonChildCounts, (v) => v)}', + ); + } + } +} + class _FlexAdjustFastPathProfiler { static int _attempts = 0; static int _hits = 0; @@ -672,6 +893,16 @@ class _FlexIntrinsicMeasurementLookupResult { final int? sharedPlainTextSignature; } +class _FlexMeasuredLayoutSlotEntry { + const _FlexMeasuredLayoutSlotEntry({ + required this.measuredConstraints, + required this.measuredSize, + }); + + final BoxConstraints measuredConstraints; + final Size measuredSize; +} + // Position and size info of each run (flex line) in flex layout. // https://www.w3.org/TR/css-flexbox-1/#flex-lines class _RunMetrics { @@ -1255,6 +1486,9 @@ class RenderFlexLayout extends RenderLayoutBox { 'childrenIntrinsicMeasureCache'); final Map _childrenRequirePostMeasureLayout = Map.identity(); + final Map + _childrenMeasuredLayoutSlots = + Map.identity(); int _adjustedConstraintsCachePassId = -1; final Map _adjustedConstraintsCache = @@ -1262,6 +1496,8 @@ class RenderFlexLayout extends RenderLayoutBox { Map? _transientChildSizeOverrides; Map? _metricsOnlyIntrinsicMeasureChildEligibilityCache; Map? _cacheableIntrinsicMeasureFlowChildCache; + Map? _cacheableMeasuredLayoutFlexChildCache; + Map? _measuredLayoutSlotEligibilityCache; int _wrappingFlexAncestorCachePassId = -1; bool? _hasWrappingFlexAncestorCached; int _reusableIntrinsicStyleSignaturePassId = -1; @@ -1282,11 +1518,14 @@ class RenderFlexLayout extends RenderLayoutBox { Expando<_FlexIntrinsicMeasurementCacheBucket>( 'childrenIntrinsicMeasureCache'); _childrenRequirePostMeasureLayout.clear(); + _childrenMeasuredLayoutSlots.clear(); _adjustedConstraintsCachePassId = -1; _adjustedConstraintsCache.clear(); _transientChildSizeOverrides = null; _metricsOnlyIntrinsicMeasureChildEligibilityCache = null; _cacheableIntrinsicMeasureFlowChildCache = null; + _cacheableMeasuredLayoutFlexChildCache = null; + _measuredLayoutSlotEligibilityCache = null; _wrappingFlexAncestorCachePassId = -1; _hasWrappingFlexAncestorCached = null; _reusableIntrinsicStyleSignaturePassId = -1; @@ -1872,6 +2111,36 @@ class RenderFlexLayout extends RenderLayoutBox { return (a - b).abs() < 0.5; } + bool _shouldClearIntrinsicInvalidationAfterFlexMeasurement( + RenderBox child, { + RenderBoxModel? effectiveChild, + }) { + if (!_isHorizontalFlexDirection || + renderStyle.flexWrap != FlexWrap.nowrap) { + return false; + } + if (_hasWrappingFlexAncestor()) { + return false; + } + + effectiveChild ??= child is RenderBoxModel + ? child + : (child is RenderEventListener + ? child.child as RenderBoxModel? + : null); + if (effectiveChild == null) { + return false; + } + + if (effectiveChild.renderStyle.isSelfRenderWidget() || + effectiveChild.renderStyle.isSelfRenderReplaced() || + effectiveChild.renderStyle.position == CSSPositionType.sticky) { + return false; + } + + return true; + } + bool _tightensMainAxisToCurrentSizeWithoutCrossChange( BoxConstraints applied, BoxConstraints candidate, @@ -2710,6 +2979,37 @@ class RenderFlexLayout extends RenderLayoutBox { return resolvedFlowChild; } + RenderFlexLayout? _getMeasuredLayoutSlotFlexChild(RenderBox child) { + final Map? flexChildCache = + _cacheableMeasuredLayoutFlexChildCache; + if (flexChildCache != null && flexChildCache.containsKey(child)) { + return flexChildCache[child]; + } + + RenderFlexLayout? resolvedFlexChild; + RenderBox? current = child; + int depth = 0; + while (current != null && depth < 3) { + if (current is RenderFlexLayout) { + resolvedFlexChild = identical(current, this) ? null : current; + break; + } + + if (current is RenderEventListener || current is RenderLayoutBoxWrapper) { + current = (current as dynamic).child as RenderBox?; + depth++; + continue; + } + + break; + } + + if (flexChildCache != null) { + flexChildCache[child] = resolvedFlexChild; + } + return resolvedFlexChild; + } + bool _canReuseAnonymousFlowMeasurement(RenderFlowLayout flowChild) { Element? parentElement = flowChild.renderStyle.target.parentElement; // Button-owned anonymous wrappers still regress :hover/:active snapshots @@ -2891,6 +3191,70 @@ class RenderFlexLayout extends RenderLayoutBox { return false; } + bool _hasPercentageSensitiveFlexPromotionSubtree(RenderObject? node) { + if (node == null || node is RenderPositionPlaceholder) { + return false; + } + + if (node is RenderEventListener || node is RenderLayoutBoxWrapper) { + return _hasPercentageSensitiveFlexPromotionSubtree( + (node as dynamic).child as RenderObject?, + ); + } + + if (node is RenderTextBox) { + return false; + } + + if (node is RenderBoxModel) { + final CSSRenderStyle style = node.renderStyle; + if (style.position == CSSPositionType.absolute || + style.position == CSSPositionType.fixed || + style.position == CSSPositionType.sticky || + style.isSelfRenderWidget() || + style.isSelfRenderReplaced()) { + return true; + } + if (style.width.type == CSSLengthType.PERCENTAGE || + style.height.type == CSSLengthType.PERCENTAGE || + style.minWidth.type == CSSLengthType.PERCENTAGE || + style.minHeight.type == CSSLengthType.PERCENTAGE || + style.maxWidth.type == CSSLengthType.PERCENTAGE || + style.maxHeight.type == CSSLengthType.PERCENTAGE || + style.marginLeft.type == CSSLengthType.PERCENTAGE || + style.marginRight.type == CSSLengthType.PERCENTAGE || + style.marginTop.type == CSSLengthType.PERCENTAGE || + style.marginBottom.type == CSSLengthType.PERCENTAGE || + style.paddingLeft.type == CSSLengthType.PERCENTAGE || + style.paddingRight.type == CSSLengthType.PERCENTAGE || + style.paddingTop.type == CSSLengthType.PERCENTAGE || + style.paddingBottom.type == CSSLengthType.PERCENTAGE || + style.flexBasis?.type == CSSLengthType.PERCENTAGE) { + return true; + } + } + + if (node is ContainerRenderObjectMixin>) { + RenderBox? child = (node as dynamic).firstChild as RenderBox?; + while (child != null) { + if (_hasPercentageSensitiveFlexPromotionSubtree(child)) { + return true; + } + child = (node as dynamic).childAfter(child) as RenderBox?; + } + return false; + } + + if (node is RenderObjectWithChildMixin) { + return _hasPercentageSensitiveFlexPromotionSubtree( + (node as dynamic).child as RenderBox?, + ); + } + + return false; + } + bool _hasWrappingFlexAncestor() { if (renderBoxModelInLayoutStack.isNotEmpty) { final int currentLayoutPassId = renderBoxModelLayoutPassId; @@ -3434,7 +3798,86 @@ class RenderFlexLayout extends RenderLayoutBox { return child is RenderEventListener || child is RenderLayoutBoxWrapper; } - void _layoutChildForFlex(RenderBox child, BoxConstraints childConstraints) { + bool _isMeasuredLayoutSlotEligible(RenderBox child) { + final Map? eligibilityCache = + _measuredLayoutSlotEligibilityCache; + final int? cachedValue = eligibilityCache?[child]; + if (cachedValue != null) { + return cachedValue == 1; + } + + final RenderFlexLayout? nestedFlex = _getMeasuredLayoutSlotFlexChild(child); + bool isEligible = false; + if (nestedFlex != null && + nestedFlex._isHorizontalFlexDirection && + nestedFlex.renderStyle.flexWrap == FlexWrap.nowrap && + nestedFlex.renderStyle.alignItems != AlignItems.stretch && + nestedFlex.renderStyle.alignItems != AlignItems.baseline && + nestedFlex.renderStyle.alignItems != AlignItems.lastBaseline && + !_hasPercentageSensitiveFlexPromotionSubtree(nestedFlex)) { + isEligible = true; + } + + eligibilityCache?[child] = isEligible ? 1 : 0; + return isEligible; + } + + void _storeMeasuredLayoutSlot( + RenderBox child, + BoxConstraints childConstraints, + Size childSize, + ) { + if (!_isMeasuredLayoutSlotEligible(child)) { + return; + } + _childrenMeasuredLayoutSlots[child] = _FlexMeasuredLayoutSlotEntry( + measuredConstraints: childConstraints, + measuredSize: Size.copy(childSize), + ); + } + + bool _canPromoteMeasuredLayoutSlot( + RenderBox child, + RenderBoxModel effectiveChild, + BoxConstraints childConstraints, + ) { + final _FlexMeasuredLayoutSlotEntry? slot = + _childrenMeasuredLayoutSlots[child]; + if (slot == null) { + return false; + } + if (!_isMeasuredLayoutSlotEligible(child) || + !canReuseStableProxyChildLayout(child, slot.measuredConstraints) || + effectiveChild.needsRelayout || + (_childrenRequirePostMeasureLayout[child] == true) || + _subtreeHasPendingIntrinsicMeasureInvalidation(child)) { + return false; + } + if (child.constraints != slot.measuredConstraints) { + return false; + } + + final double currentMainSize = _isHorizontalFlexDirection + ? slot.measuredSize.width + : slot.measuredSize.height; + if (!_tightensMainAxisToCurrentSizeWithoutCrossChange( + slot.measuredConstraints, + childConstraints, + currentMainSize, + )) { + return false; + } + + _childrenRequirePostMeasureLayout[child] = false; + return true; + } + + void _layoutChildForFlex( + RenderBox child, + BoxConstraints childConstraints, { + _FlexLayoutChildReason reason = _FlexLayoutChildReason.computeRunMetrics, + }) { + _FlexLayoutChildProfiler.record(reason, child, childConstraints); if (child is RenderBoxModel) { child.setRelayoutParentOnSizeChange( _shouldAvoidParentUsesSizeForFlexChild(child) ? this : null, @@ -3445,6 +3888,7 @@ class RenderFlexLayout extends RenderLayoutBox { parentUsesSize: !_shouldAvoidParentUsesSizeForFlexChild(child), ); _childrenRequirePostMeasureLayout[child] = false; + _childrenMeasuredLayoutSlots.remove(child); } @pragma('vm:prefer-inline') @@ -3453,10 +3897,7 @@ class RenderFlexLayout extends RenderLayoutBox { BoxConstraints childConstraints, { RenderBoxModel? effectiveChild, }) { - if (!child.hasSize || child.constraints != childConstraints) { - return false; - } - if (child.debugNeedsLayout) { + if (!canReuseStableProxyChildLayout(child, childConstraints)) { return false; } if (effectiveChild != null && effectiveChild.needsRelayout) { @@ -3909,7 +4350,27 @@ class RenderFlexLayout extends RenderLayoutBox { childConstraints, effectiveChild: effectiveChild, )) { - _layoutChildForFlex(child, childConstraints); + _FlexMeasurementReuseProfiler.recordMiss( + _FlexLayoutChildReason.computeRunMetrics, + child, + childConstraints, + effectiveChild: effectiveChild, + requiresPostMeasureLayout: + _childrenRequirePostMeasureLayout[child] == true, + subtreeHasPendingIntrinsicInvalidation: + _subtreeHasPendingIntrinsicMeasureInvalidation(child), + ); + _layoutChildForFlex( + child, + childConstraints, + reason: _FlexLayoutChildReason.computeRunMetrics, + ); + if (_shouldClearIntrinsicInvalidationAfterFlexMeasurement( + child, + effectiveChild: effectiveChild, + )) { + _clearSubtreeIntrinsicMeasurementInvalidationAfterMeasurement(child); + } } _cacheOriginalConstraintsIfNeeded(child, childConstraints); @@ -3956,6 +4417,251 @@ class RenderFlexLayout extends RenderLayoutBox { return runMetrics; } + List<_RunMetrics>? _tryBuildEarlySingleFlexibleGrowRunMetrics( + List children, + ) { + if (!_isHorizontalFlexDirection || + renderStyle.flexWrap != FlexWrap.nowrap) { + return null; + } + + final _FlexResolutionInputs inputs = _computeFlexResolutionInputs(); + final double? maxMainSize = inputs.maxMainSize; + if (maxMainSize == null || !inputs.isMainSizeDefinite) { + return null; + } + + final bool boundedOnly = !(inputs.contentBoxLogicalWidth != null || + (contentConstraints?.hasTightWidth ?? false) || + constraints.hasTightWidth); + if (boundedOnly) { + return null; + } + + final double mainAxisGap = _getMainAxisGap(); + final List<_RunChild> runChildren = <_RunChild>[]; + int? growChildIndex; + RenderBox? growChild; + RenderBoxModel? growEffectiveChild; + _RunChild? growRunChild; + double runCrossAxisExtent = 0.0; + double occupiedMainAxisExtent = 0.0; + + for (int childIndex = 0; childIndex < children.length; childIndex++) { + final RenderBox child = children[childIndex]; + final RenderBoxModel? effectiveChild = + child is RenderBoxModel ? child : null; + final BoxConstraints baseConstraints = effectiveChild != null + ? effectiveChild.getConstraints() + : constraints; + + final _FlexFastPathRejectReason? rejectReason = + _getEarlyNoFlexNoStretchNoBaselineRejectReason( + child, + baseConstraints, + ); + if (rejectReason != null) { + return null; + } + + final double flexGrow = _getFlexGrow(child); + if (flexGrow > 0) { + if (growChild != null || effectiveChild == null) { + return null; + } + if (child is RenderPositionPlaceholder || + effectiveChild.renderStyle.position == CSSPositionType.sticky || + effectiveChild.renderStyle.isSelfRenderWidget() || + effectiveChild.renderStyle.isSelfRenderReplaced()) { + return null; + } + final AlignSelf alignSelf = _getAlignSelf(child); + if (alignSelf == AlignSelf.baseline || + alignSelf == AlignSelf.lastBaseline || + alignSelf == AlignSelf.stretch) { + return null; + } + + final _RunChild candidateRunChild = _createRunChildMetadata( + child, + 0, + effectiveChild: effectiveChild, + usedFlexBasis: _getUsedFlexBasis(child), + ); + if (candidateRunChild.hasAutoMainAxisMargin || + candidateRunChild.hasAutoCrossAxisMargin || + _needToStretchChildCrossSize(effectiveChild)) { + return null; + } + + growChildIndex = childIndex; + growChild = child; + growEffectiveChild = effectiveChild; + growRunChild = candidateRunChild; + continue; + } + + final BoxConstraints childConstraints = _getIntrinsicConstraints(child); + if (!_canReuseCurrentFlexMeasurement( + child, + childConstraints, + effectiveChild: effectiveChild, + )) { + _FlexMeasurementReuseProfiler.recordMiss( + _FlexLayoutChildReason.computeRunMetrics, + child, + childConstraints, + effectiveChild: effectiveChild, + requiresPostMeasureLayout: + _childrenRequirePostMeasureLayout[child] == true, + subtreeHasPendingIntrinsicInvalidation: + _subtreeHasPendingIntrinsicMeasureInvalidation(child), + ); + _layoutChildForFlex( + child, + childConstraints, + reason: _FlexLayoutChildReason.computeRunMetrics, + ); + } + _cacheOriginalConstraintsIfNeeded(child, childConstraints); + + final RenderLayoutParentData? childParentData = + child.parentData as RenderLayoutParentData?; + childParentData?.runIndex = 0; + + final Size childSize = _getChildSize(child)!; + final double childMainSize = childSize.width; + final double childCrossSize = childSize.height; + _childrenIntrinsicMainSizes[child] = childMainSize; + + final _RunChild runChild = _createRunChildMetadata( + child, + childMainSize, + effectiveChild: effectiveChild, + usedFlexBasis: effectiveChild != null ? _getUsedFlexBasis(child) : null, + ); + occupiedMainAxisExtent += + childMainSize + runChild.mainAxisExtentAdjustment; + runCrossAxisExtent = math.max( + runCrossAxisExtent, + childCrossSize + runChild.crossAxisExtentAdjustment, + ); + runChildren.add(runChild); + } + + if (growChild == null || + growChildIndex == null || + growEffectiveChild == null || + growRunChild == null) { + return null; + } + + final int itemCount = children.length; + final double totalGapSpacing = + itemCount > 1 ? (itemCount - 1) * mainAxisGap : 0.0; + final double growMainAxisExtentAdjustment = + growRunChild.mainAxisExtentAdjustment; + final double targetGrowMainSize = maxMainSize - + occupiedMainAxisExtent - + totalGapSpacing - + growMainAxisExtentAdjustment; + if (!targetGrowMainSize.isFinite || targetGrowMainSize <= 0) { + return null; + } + + double growFinalMainSize = targetGrowMainSize; + final double minMainSize = _getRunChildMinMainAxisSize(growRunChild); + final double maxMainAxisSize = _getRunChildMaxMainAxisSize(growRunChild); + if (growFinalMainSize < minMainSize) { + growFinalMainSize = minMainSize; + } + if (maxMainAxisSize.isFinite && growFinalMainSize > maxMainAxisSize) { + growFinalMainSize = maxMainAxisSize; + } + + _childrenOldConstraints[growEffectiveChild] = + growEffectiveChild.getConstraints(); + final BoxConstraints growChildConstraints = _getChildAdjustedConstraints( + growEffectiveChild, + growFinalMainSize, + null, + itemCount, + ); + if (!_canReuseCurrentFlexMeasurement( + growChild, + growChildConstraints, + effectiveChild: growEffectiveChild, + )) { + _FlexMeasurementReuseProfiler.recordMiss( + _FlexLayoutChildReason.singleFlexibleRun, + growChild, + growChildConstraints, + effectiveChild: growEffectiveChild, + requiresPostMeasureLayout: + _childrenRequirePostMeasureLayout[growChild] == true, + subtreeHasPendingIntrinsicInvalidation: + _subtreeHasPendingIntrinsicMeasureInvalidation(growChild), + ); + _layoutChildForFlex( + growChild, + growChildConstraints, + reason: _FlexLayoutChildReason.singleFlexibleRun, + ); + if (_shouldClearIntrinsicInvalidationAfterFlexMeasurement( + growChild, + effectiveChild: growEffectiveChild, + )) { + _clearSubtreeIntrinsicMeasurementInvalidationAfterMeasurement( + growChild); + } + } + _cacheOriginalConstraintsIfNeeded(growChild, growChildConstraints); + + final RenderLayoutParentData? growChildParentData = + growChild.parentData as RenderLayoutParentData?; + growChildParentData?.runIndex = 0; + + final Size growChildSize = _getChildSize(growChild)!; + final double growChildMainSize = growChildSize.width; + _childrenIntrinsicMainSizes[growChild] = growChildMainSize; + + growRunChild.originalMainSize = growChildMainSize; + growRunChild.flexedMainSize = growChildMainSize; + + runChildren.insert(growChildIndex, growRunChild); + + double runMainAxisExtent = 0.0; + double finalCrossAxisExtent = 0.0; + for (int index = 0; index < runChildren.length; index++) { + if (index > 0) { + runMainAxisExtent += mainAxisGap; + } + final _RunChild runChild = runChildren[index]; + final Size childSize = _getChildSize(runChild.child)!; + runMainAxisExtent += childSize.width + runChild.mainAxisExtentAdjustment; + finalCrossAxisExtent = math.max( + finalCrossAxisExtent, + childSize.height + runChild.crossAxisExtentAdjustment, + ); + } + + final List<_RunMetrics> runMetrics = <_RunMetrics>[ + _RunMetrics( + runMainAxisExtent, + finalCrossAxisExtent, + 0, + 0, + 0, + runChildren, + 0, + ) + ]; + + _flexLineBoxMetrics = runMetrics; + _storePercentageConstraintChildrenOldConstraints(children); + return runMetrics; + } + @override void performLayout() { try { @@ -4109,6 +4815,7 @@ class RenderFlexLayout extends RenderLayoutBox { // 4. Align children according to alignment properties. void _layoutFlexItems(List children) { _childrenRequirePostMeasureLayout.clear(); + _childrenMeasuredLayoutSlots.clear(); _childrenIntrinsicMainSizes.clear(); if (_adjustedConstraintsCachePassId != renderBoxModelLayoutPassId) { _adjustedConstraintsCachePassId = renderBoxModelLayoutPassId; @@ -4162,6 +4869,10 @@ class RenderFlexLayout extends RenderLayoutBox { } } + if (runMetrics == null) { + runMetrics = _tryBuildEarlySingleFlexibleGrowRunMetrics(children); + } + if (runMetrics == null) { // Layout children to compute metrics of flex lines. if (_enableFlexProfileSections) { @@ -4517,6 +5228,9 @@ class RenderFlexLayout extends RenderLayoutBox { Map.identity(); _cacheableIntrinsicMeasureFlowChildCache = Map.identity(); + _cacheableMeasuredLayoutFlexChildCache = + Map.identity(); + _measuredLayoutSlotEligibilityCache = Map.identity(); final bool allowAnonymousMetricsOnlyCache = _canUseAnonymousMetricsOnlyCache(children); _transientChildSizeOverrides = Map.identity(); @@ -4565,7 +5279,29 @@ class RenderFlexLayout extends RenderLayoutBox { childConstraints, effectiveChild: effectiveChild, )) { - _layoutChildForFlex(child, childConstraints); + _FlexMeasurementReuseProfiler.recordMiss( + _FlexLayoutChildReason.computeRunMetrics, + child, + childConstraints, + effectiveChild: effectiveChild, + requiresPostMeasureLayout: + _childrenRequirePostMeasureLayout[child] == true, + subtreeHasPendingIntrinsicInvalidation: + _subtreeHasPendingIntrinsicMeasureInvalidation(child), + ); + _layoutChildForFlex( + child, + childConstraints, + reason: _FlexLayoutChildReason.computeRunMetrics, + ); + if (_shouldClearIntrinsicInvalidationAfterFlexMeasurement( + child, + effectiveChild: effectiveChild, + )) { + _clearSubtreeIntrinsicMeasurementInvalidationAfterMeasurement( + child, + ); + } } if (isMetricsOnlyMeasureChild && renderStyle.flexWrap == FlexWrap.nowrap && @@ -4582,6 +5318,7 @@ class RenderFlexLayout extends RenderLayoutBox { childSize = Size.copy(_getChildSize(child)!); _transientChildSizeOverrides![child] = childSize; intrinsicMain = isHorizontal ? childSize.width : childSize.height; + _storeMeasuredLayoutSlot(child, childConstraints, childSize); // CSS Flexbox §9.2: For flex-basis:auto with an auto main-size, the flex base size // should come from the item's max-content contribution in the main axis, not from @@ -5330,7 +6067,7 @@ class RenderFlexLayout extends RenderLayoutBox { relayoutDetails = _describeFirstPendingIntrinsicMeasureInvalidation(child); } else if (desiredPreservedMain != null && - desiredPreservedMain != childOldMainSize) { + (desiredPreservedMain - childOldMainSize).abs() >= 0.5) { needsLayout = true; relayoutReason = _FlexAdjustFastPathRelayoutReason.preservedMainMismatch; @@ -5439,7 +6176,11 @@ class RenderFlexLayout extends RenderLayoutBox { runChildrenCount, preserveMainAxisSize: desiredPreservedMain, ); - _layoutChildForFlex(child, childConstraints); + _layoutChildForFlex( + child, + childConstraints, + reason: _FlexLayoutChildReason.adjustPhase1, + ); _clearSubtreeIntrinsicMeasurementInvalidationAfterMeasurement(child); didRelayout = true; } @@ -5478,6 +6219,110 @@ class RenderFlexLayout extends RenderLayoutBox { return true; } + bool _trySingleFlexibleRunFastPath( + _RunMetrics metrics, + _RunChild flexibleRunChild, + ) { + final List<_RunChild> runChildrenList = metrics.runChildren; + if (runChildrenList.isEmpty) { + return false; + } + + bool didRelayout = false; + + for (final _RunChild runChild in runChildrenList) { + final RenderBox child = runChild.child; + final RenderBoxModel effectiveChild = runChild.effectiveChild!; + final bool isFlexibleChild = identical(runChild, flexibleRunChild); + final Size childSize = _getChildSize(child)!; + final double childOldMainSize = childSize.width; + + final double? childFlexedMainSize = + isFlexibleChild ? runChild.flexedMainSize : null; + double? desiredPreservedMain; + if (!isFlexibleChild) { + desiredPreservedMain = _childrenIntrinsicMainSizes[child]; + } + + bool needsLayout = isFlexibleChild || + effectiveChild.needsRelayout || + (_childrenRequirePostMeasureLayout[child] == true) || + _subtreeHasPendingIntrinsicMeasureInvalidation(child); + + if (!needsLayout && + desiredPreservedMain != null && + (desiredPreservedMain - childOldMainSize).abs() >= 0.5) { + needsLayout = true; + } + + if (!needsLayout && desiredPreservedMain != null) { + final BoxConstraints applied = child.constraints; + final bool autoMain = effectiveChild.renderStyle.width.isAuto; + final bool wasNonTightMain = !applied.hasTightWidth; + if (autoMain && wasNonTightMain) { + needsLayout = true; + } + } + + if (!needsLayout) { + continue; + } + + _markFlexRelayoutForTextOnly(effectiveChild); + + final BoxConstraints childConstraints = _getChildAdjustedConstraints( + effectiveChild, + childFlexedMainSize, + null, + runChildrenList.length, + preserveMainAxisSize: desiredPreservedMain, + ); + if (_canSkipAdjustedFlexChildLayout( + child, + effectiveChild, + childConstraints, + ) || + _canPromoteMeasuredLayoutSlot( + child, + effectiveChild, + childConstraints, + )) { + continue; + } + + _layoutChildForFlex( + child, + childConstraints, + reason: _FlexLayoutChildReason.singleFlexibleRun, + ); + if (_shouldClearIntrinsicInvalidationAfterFlexMeasurement( + child, + effectiveChild: effectiveChild, + )) { + _clearSubtreeIntrinsicMeasurementInvalidationAfterMeasurement(child); + } + didRelayout = true; + } + + if (!didRelayout) { + return true; + } + + double mainAxisExtent = 0; + final double mainAxisGap = _getMainAxisGap(); + for (int i = 0; i < runChildrenList.length; i++) { + if (i > 0) { + mainAxisExtent += mainAxisGap; + } + final _RunChild runChild = runChildrenList[i]; + final Size childSize = _getChildSize(runChild.child)!; + mainAxisExtent += childSize.width + runChild.mainAxisExtentAdjustment; + } + metrics.mainAxisExtent = mainAxisExtent; + metrics.crossAxisExtent = _recomputeRunCrossExtent(metrics); + return true; + } + // Adjust children size (not include position placeholder) based on // flex factors (flex-grow/flex-shrink) and alignment in cross axis (align-items). // https://www.w3.org/TR/css-flexbox-1/#resolve-flexible-lengths @@ -5527,6 +6372,11 @@ class RenderFlexLayout extends RenderLayoutBox { final double totalFlexGrow = metrics.totalFlexGrow; final double totalFlexShrink = metrics.totalFlexShrink; final List<_RunChild> runChildrenList = metrics.runChildren; + bool canUseSingleFlexibleRunFastPath = _isHorizontalFlexDirection; + _RunChild? singleGrowRunChild; + _RunChild? singleShrinkRunChild; + int growChildCount = 0; + int shrinkChildCount = 0; double totalSpace = 0; // Flex factor calculation depends on flex-basis if exists. @@ -5534,6 +6384,47 @@ class RenderFlexLayout extends RenderLayoutBox { final double childSpace = runChild.usedFlexBasis ?? runChild.originalMainSize; totalSpace += childSpace + runChild.mainAxisMargin; + + if (!canUseSingleFlexibleRunFastPath) { + continue; + } + + final RenderBoxModel? effectiveChild = runChild.effectiveChild; + if (effectiveChild == null || + runChild.hasAutoMainAxisMargin || + runChild.hasAutoCrossAxisMargin || + runChild.child is RenderPositionPlaceholder || + runChild.isReplaced || + effectiveChild.renderStyle.position == CSSPositionType.sticky || + effectiveChild.renderStyle.isSelfRenderWidget() || + _needToStretchChildCrossSize(effectiveChild)) { + canUseSingleFlexibleRunFastPath = false; + continue; + } + + final AlignSelf alignSelf = runChild.alignSelf; + if (alignSelf == AlignSelf.baseline || + alignSelf == AlignSelf.lastBaseline || + alignSelf == AlignSelf.stretch) { + canUseSingleFlexibleRunFastPath = false; + continue; + } + + if (runChild.flexGrow > 0) { + growChildCount++; + singleGrowRunChild = runChild; + if (growChildCount > 1) { + canUseSingleFlexibleRunFastPath = false; + } + } + + if (runChild.flexShrink > 0) { + shrinkChildCount++; + singleShrinkRunChild = runChild; + if (shrinkChildCount > 1) { + canUseSingleFlexibleRunFastPath = false; + } + } } // Add gap spacing to total space calculation for flex-grow available space @@ -5687,6 +6578,37 @@ class RenderFlexLayout extends RenderLayoutBox { _resolveFlexibleLengths(metrics, totalFlexFactor, usedFreeSpace)) {} } + final bool hasResolvedSingleGrowChild = singleGrowRunChild != null && + (singleGrowRunChild.flexedMainSize - + singleGrowRunChild.originalMainSize) + .abs() >= + 0.5; + if (isFlexGrow && + !isFlexShrink && + canUseSingleFlexibleRunFastPath && + growChildCount == 1 && + hasResolvedSingleGrowChild && + _trySingleFlexibleRunFastPath(metrics, singleGrowRunChild!)) { + continue; + } + + final bool hasResolvedSingleShrinkChild = singleShrinkRunChild != null && + (singleShrinkRunChild.originalMainSize - + singleShrinkRunChild.flexedMainSize) + .abs() >= + 0.5 && + singleShrinkRunChild.flexedMainSize < + singleShrinkRunChild.originalMainSize; + if (!isFlexGrow && + isFlexShrink && + canUseSingleFlexibleRunFastPath && + growChildCount == 0 && + shrinkChildCount == 1 && + hasResolvedSingleShrinkChild && + _trySingleFlexibleRunFastPath(metrics, singleShrinkRunChild!)) { + continue; + } + // Phase 1 — Relayout each item with its resolved main size only. // Do not apply align-items: stretch yet, so text/content can expand to // its natural height based on the final line width. @@ -5719,7 +6641,7 @@ class RenderFlexLayout extends RenderLayoutBox { (_childrenRequirePostMeasureLayout[child] == true); if (!needsLayout && desiredPreservedMain != null && - (desiredPreservedMain != childOldMainSize)) { + (desiredPreservedMain - childOldMainSize).abs() >= 0.5) { needsLayout = true; } if (!needsLayout && desiredPreservedMain != null) { @@ -5767,13 +6689,28 @@ class RenderFlexLayout extends RenderLayoutBox { preserveMainAxisSize: desiredPreservedMain, ); if (_canSkipAdjustedFlexChildLayout( + child, + effectiveChild, + childConstraints, + ) || + _canPromoteMeasuredLayoutSlot( + child, + effectiveChild, + childConstraints, + )) { + continue; + } + _layoutChildForFlex( child, - effectiveChild, childConstraints, + reason: _FlexLayoutChildReason.adjustPhase1, + ); + if (_shouldClearIntrinsicInvalidationAfterFlexMeasurement( + child, + effectiveChild: effectiveChild, )) { - continue; + _clearSubtreeIntrinsicMeasurementInvalidationAfterMeasurement(child); } - _layoutChildForFlex(child, childConstraints); } // After Phase 1, recompute the run cross extent based on the items’ natural @@ -5816,13 +6753,28 @@ class RenderFlexLayout extends RenderLayoutBox { runChildrenList.length, ); if (_canSkipAdjustedFlexChildLayout( + child, + effectiveChild, + childConstraints, + ) || + _canPromoteMeasuredLayoutSlot( + child, + effectiveChild, + childConstraints, + )) { + continue; + } + _layoutChildForFlex( child, - effectiveChild, childConstraints, + reason: _FlexLayoutChildReason.adjustStretch, + ); + if (_shouldClearIntrinsicInvalidationAfterFlexMeasurement( + child, + effectiveChild: effectiveChild, )) { - continue; + _clearSubtreeIntrinsicMeasurementInvalidationAfterMeasurement(child); } - _layoutChildForFlex(child, childConstraints); } // Finally, recompute run main & cross extents using the final sizes. diff --git a/webf/lib/src/rendering/flow.dart b/webf/lib/src/rendering/flow.dart index 741dd39b97..d81e0dadb7 100644 --- a/webf/lib/src/rendering/flow.dart +++ b/webf/lib/src/rendering/flow.dart @@ -19,21 +19,7 @@ import 'package:webf/foundation.dart'; import 'package:webf/rendering.dart'; bool _canReuseFlowChildLayout(RenderBox child, BoxConstraints constraints) { - if (!child.hasSize || child.constraints != constraints) { - return false; - } - if (child.debugNeedsLayout) { - return false; - } - if (child is RenderTextBox && child.hasPendingTextLayoutUpdate) { - return false; - } - if (child is RenderBoxModel && - (child.needsRelayout || - child.hasPendingSubtreeIntrinsicMeasurementInvalidation)) { - return false; - } - return true; + return canReuseStableProxyChildLayout(child, constraints); } bool _shouldAvoidParentUsesSizeForFlowChild(RenderBox child) { From 94aa230cad1eaf179c861c2a748e9c4633d6ec8a Mon Sep 17 00:00:00 2001 From: andycall Date: Sun, 29 Mar 2026 08:08:08 -0700 Subject: [PATCH 13/13] fix(webf): correct widget layout reuse in flex --- webf/lib/src/rendering/box_model.dart | 7 +++ webf/lib/src/rendering/flex.dart | 61 ++++++++++++++++++++++++--- webf/lib/src/rendering/text.dart | 35 ++++++++++----- 3 files changed, 85 insertions(+), 18 deletions(-) diff --git a/webf/lib/src/rendering/box_model.dart b/webf/lib/src/rendering/box_model.dart index 646110bc52..28f50605e5 100644 --- a/webf/lib/src/rendering/box_model.dart +++ b/webf/lib/src/rendering/box_model.dart @@ -131,6 +131,13 @@ bool _hasReusableStableLayoutChain(RenderBox child) { if (child is RenderTextBox) { return !child.hasPendingTextLayoutUpdate; } + if (child is RenderWidget) { + if (child.renderStyle.width.isAuto || child.renderStyle.height.isAuto) { + return false; + } + return !child.needsRelayout && + !child.hasPendingSubtreeIntrinsicMeasurementInvalidation; + } if (child is RenderBoxModel) { return !child.needsRelayout && !child.hasPendingSubtreeIntrinsicMeasurementInvalidation; diff --git a/webf/lib/src/rendering/flex.dart b/webf/lib/src/rendering/flex.dart index 534d9c1337..3116012ed6 100644 --- a/webf/lib/src/rendering/flex.dart +++ b/webf/lib/src/rendering/flex.dart @@ -4283,11 +4283,16 @@ class RenderFlexLayout extends RenderLayoutBox { continue; } - final bool hasPercentageMaxWidth = - box.renderStyle.maxWidth.type == CSSLengthType.PERCENTAGE; - final bool hasPercentageMaxHeight = - box.renderStyle.maxHeight.type == CSSLengthType.PERCENTAGE; - if (!hasPercentageMaxWidth && !hasPercentageMaxHeight) { + final CSSRenderStyle style = box.renderStyle; + final bool hasPercentageConstraint = + style.width.type == CSSLengthType.PERCENTAGE || + style.height.type == CSSLengthType.PERCENTAGE || + style.minWidth.type == CSSLengthType.PERCENTAGE || + style.minHeight.type == CSSLengthType.PERCENTAGE || + style.maxWidth.type == CSSLengthType.PERCENTAGE || + style.maxHeight.type == CSSLengthType.PERCENTAGE || + style.flexBasis?.type == CSSLengthType.PERCENTAGE; + if (!hasPercentageConstraint) { continue; } @@ -7046,6 +7051,46 @@ class RenderFlexLayout extends RenderLayoutBox { } } + if (_isHorizontalFlexDirection && + childFlexedMainSize == null && + child.renderStyle.width.type == CSSLengthType.PERCENTAGE) { + double containerContentW; + if (contentConstraints != null && + contentConstraints!.maxWidth.isFinite) { + containerContentW = contentConstraints!.maxWidth; + } else { + final double borderW = + renderStyle.effectiveBorderLeftWidth.computedValue + + renderStyle.effectiveBorderRightWidth.computedValue; + containerContentW = math.max(0, size.width - borderW); + } + + final double percent = child.renderStyle.width.value ?? 0; + double childBorderBoxW = + containerContentW.isFinite ? (containerContentW * percent) : 0; + if (!childBorderBoxW.isFinite || childBorderBoxW < 0) { + childBorderBoxW = 0; + } + + if (child.renderStyle.maxWidth.isNotNone && + child.renderStyle.maxWidth.type != CSSLengthType.PERCENTAGE) { + childBorderBoxW = math.min( + childBorderBoxW, + child.renderStyle.maxWidth.computedValue, + ); + } + if (child.renderStyle.minWidth.isNotAuto) { + childBorderBoxW = math.max( + childBorderBoxW, + child.renderStyle.minWidth.computedValue, + ); + } + + minConstraintWidth = childBorderBoxW; + maxConstraintWidth = childBorderBoxW; + _overrideChildContentBoxLogicalWidth(child, childBorderBoxW); + } + // Enforce the automatic minimum size on the main axis when flex-direction is column // and the item did not flex in the main axis. This prevents a definite flex-basis: 0 // from collapsing the item’s height below its content-based minimum (min-height: auto). @@ -8359,7 +8404,6 @@ class RenderFlexLayout extends RenderLayoutBox { mainAxisContentSize - leadingSpace : leadingSpace + mainAxisStartPadding + mainAxisStartBorder; - final bool runAllChildrenAtMaxCross = _areAllRunChildrenAtMaxCrossExtent( runChildrenList, runCrossAxisExtent); @@ -9053,6 +9097,10 @@ class RenderFlexLayout extends RenderLayoutBox { return false; } + if (_hasPercentageSensitiveFlexPromotionSubtree(child)) { + return false; + } + final double? desiredPreservedMain = _childrenIntrinsicMainSizes[child]; final Size childSize = _getChildSize(child)!; final double childMainSize = @@ -9181,7 +9229,6 @@ class RenderFlexLayout extends RenderLayoutBox { return Offset(mainAxisOffset, crossAxisOffset); } } - double? _getLineHeight(RenderBox child) { CSSLengthValue? lineHeight; if (child is RenderTextBox) { diff --git a/webf/lib/src/rendering/text.dart b/webf/lib/src/rendering/text.dart index f755a76677..196e9d05d4 100644 --- a/webf/lib/src/rendering/text.dart +++ b/webf/lib/src/rendering/text.dart @@ -33,9 +33,12 @@ class RenderTextBox extends RenderBox with RenderObjectWithChildMixin _hasPendingTextLayoutUpdate = true; _markAncestorSubtreeIntrinsicMeasurementUpdate(); _markAncestorInlineCollectionNeedsUpdate(); - parent?.markNeedsLayout(); - markNeedsLayout(); - markNeedsPaint(); + if (_paintsSelf) { + _markLayoutIfNeeded(this); + markNeedsPaint(); + } else { + _markLayoutIfNeeded(parent); + } _cachedSpan = null; _textPainter = null; } @@ -56,7 +59,6 @@ class RenderTextBox extends RenderBox with RenderObjectWithChildMixin while (ancestor != null) { if (ancestor is RenderFlowLayout) { ancestor.markNeedsCollectInlines(); - ancestor.markNeedsLayout(); if (ancestor.establishIFC) { break; } @@ -65,19 +67,30 @@ class RenderTextBox extends RenderBox with RenderObjectWithChildMixin } } + @pragma('vm:prefer-inline') + void _markLayoutIfNeeded(RenderObject? renderObject) { + if (renderObject == null) return; + if (!debugRenderObjectNeedsLayout(renderObject)) { + renderObject.markNeedsLayout(); + } + } + set data(String value) { if (_data == value) return; _data = value; _hasPendingTextLayoutUpdate = true; _markAncestorSubtreeIntrinsicMeasurementUpdate(); _markAncestorInlineCollectionNeedsUpdate(); - // Text content changed. Since text boxes are measured and painted by the - // parent's inline formatting context, notify the parent to relayout so the - // paragraph gets rebuilt with the new text content. - parent?.markNeedsLayout(); - // When not in IFC (no RenderBoxModel parent), we layout/paint ourselves. - // Ensure we relayout to rebuild TextPainter metrics. - markNeedsLayout(); + if (_paintsSelf) { + // Outside an ancestor IFC we own our own text metrics, so relayout the + // text box and let Flutter bubble size-dependent invalidation normally. + _markLayoutIfNeeded(this); + } else { + // IFC ancestors rebuild the paragraph from collected inline nodes. Only + // the owning render subtree needs layout dirtied once; direct ancestor + // markNeedsLayout on every flow ancestor violates relayout boundaries. + _markLayoutIfNeeded(parent); + } _cachedSpan = null; _textPainter = null; }