From 52c597a0d292d16f4f16c64cfff988ed14ed006e Mon Sep 17 00:00:00 2001 From: mortezaei Date: Thu, 18 Jun 2026 10:28:09 +0330 Subject: [PATCH] feat(webview): support Flutter initial_config handshake and dynamic safe area insets --- src/lib/view-paddings.ts | 73 ++++++++++++++++++++++++++++++---------- src/types/window.d.ts | 7 +++- 2 files changed, 61 insertions(+), 19 deletions(-) diff --git a/src/lib/view-paddings.ts b/src/lib/view-paddings.ts index 8aa7145..39fda25 100644 --- a/src/lib/view-paddings.ts +++ b/src/lib/view-paddings.ts @@ -56,16 +56,22 @@ function readEdges(source: EdgeSource): ViewPaddings { } /** - * فضای امن هر ضلع = بیشینه‌ی viewInsets و safeArea همان ضلع. - * منطبق بر منطق مستند: navigationBarHeight = max(insets, padding). - * هیچ عدد ثابتی استفاده نمی‌شود؛ همه‌چیز از دیتای دریافتی محاسبه می‌شود. + * مقادیر safe-area در `initial_config` (فیلد safeArea) از فلاتر در واحد منطقی + * (CSS px / dp) می‌آیند و مستقیماً برای CSS درست‌اند. اما مسیر قدیمی + * `get_view_paddings` مقادیر را در پیکسل فیزیکی (× devicePixelRatio) می‌فرستد؛ + * این تابع آن‌ها را به px منطقی برمی‌گرداند تا با CSS هم‌خوان شوند. */ -function mergeEdges(insets: ViewPaddings, safe: ViewPaddings): ViewPaddings { +function toLogical(edges: ViewPaddings): ViewPaddings { + const ratio = + typeof window !== "undefined" && window.devicePixelRatio > 0 + ? window.devicePixelRatio + : 1; + return { - top: Math.max(insets.top, safe.top), - bottom: Math.max(insets.bottom, safe.bottom), - left: Math.max(insets.left, safe.left), - right: Math.max(insets.right, safe.right), + top: edges.top / ratio, + bottom: edges.bottom / ratio, + left: edges.left / ratio, + right: edges.right / ratio, }; } @@ -83,6 +89,7 @@ class ViewPaddingsBridge { private listeners: Array<(paddings: ViewPaddings) => void> = []; private configListeners: Array<(config: InitialConfig) => void> = []; private flutterUnsubscribe?: () => void; + private hasInitialConfig = false; constructor() { this.init(); @@ -108,14 +115,34 @@ class ViewPaddingsBridge { this.flutterUnsubscribe = win.addFlutterResponseListener((event) => { if (!event || event.success === false || !event.data) return; - if (event.action === "initial_config") { - this.applyInitialConfig(event.data); - return; - } + switch (event.action) { + case "initial_config": + this.applyInitialConfig(event.data); + this.hasInitialConfig = true; + return; + + // به‌روزرسانی فضای امن هنگام چرخش/تغییر notch (px منطقی). + case "safe_area_changed": + this.applySafeArea(readEdges(event.data)); + return; + + // ارتفاع کیبورد به‌صورت زنده (px منطقی). + case "keyboard_changed": { + const height = num(event.data.height); + this.config.keyboardHeight = height; + this.applyKeyboard(height); + return; + } + + // سازگاری عقب‌رو: پاسخ ساده‌ی get_view_paddings (px فیزیکی، فقط لبه‌ها). + // اگر initial_config رسیده باشد، آن مرجع است و این نادیده گرفته می‌شود. + case "get_view_paddings": + if (this.hasInitialConfig) return; + this.applyEdges(toLogical(readEdges(event.data))); + return; - // سازگاری عقب‌رو: پاسخ ساده‌ی get_view_paddings که فقط لبه‌ها را دارد - if (event.action === "get_view_paddings") { - this.applyEdges(readEdges(event.data)); + default: + return; } }); @@ -144,13 +171,16 @@ class ViewPaddingsBridge { private applyInitialConfig(data: any) { const viewInsets = readEdges(data?.viewInsets); const safeArea = readEdges(data?.safeArea); - const paddings = mergeEdges(viewInsets, safeArea); + // فیلد safeArea از فلاتر در px منطقی است و واحد درست برای CSS؛ همین مبنا + // برای --safe-* است. (فیلد viewInsets در این payload در px فیزیکی و عملاً + // تکرار همان viewPadding است، پس در محاسبه‌ی فضای امن استفاده نمی‌شود.) + const paddings = safeArea; this.config = { paddings, viewInsets, safeArea, - keyboardHeight: viewInsets.bottom, + keyboardHeight: 0, layout: data?.layout ? { breakpoint: String(data.layout.breakpoint ?? ""), @@ -176,7 +206,14 @@ class ViewPaddingsBridge { }; this.applyEdges(paddings); - this.applyKeyboard(viewInsets.bottom); + this.applyKeyboard(0); + this.notifyConfigListeners(); + } + + private applySafeArea(safeArea: ViewPaddings) { + this.config.safeArea = safeArea; + this.config.paddings = safeArea; + this.applyEdges(safeArea); this.notifyConfigListeners(); } diff --git a/src/types/window.d.ts b/src/types/window.d.ts index caf17f7..8a1cff3 100644 --- a/src/types/window.d.ts +++ b/src/types/window.d.ts @@ -13,11 +13,16 @@ declare global { // get_location latitude?: number; longitude?: number; - // get_view_paddings (flat edges) + // get_view_paddings / safe_area_changed (flat edges) top?: number; bottom?: number; left?: number; right?: number; + // keyboard_changed + height?: number; + visible?: boolean; + // orientation_changed + orientation?: string; message?: string; timestamp?: number; // initial_config