From eba847e62ec581bd0ea539d0d2a5a31828db8d2b Mon Sep 17 00:00:00 2001 From: alonginwind <100897495+alonginwind@users.noreply.github.com> Date: Mon, 22 Dec 2025 21:08:38 +0800 Subject: [PATCH] Fix Terminal top content overlapping with notch (SafeArea) (#13724) --- flutter/lib/mobile/pages/terminal_page.dart | 89 ++++++++++++++++----- 1 file changed, 67 insertions(+), 22 deletions(-) diff --git a/flutter/lib/mobile/pages/terminal_page.dart b/flutter/lib/mobile/pages/terminal_page.dart index 17d9bbedb..35dcb04bd 100644 --- a/flutter/lib/mobile/pages/terminal_page.dart +++ b/flutter/lib/mobile/pages/terminal_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; @@ -29,9 +31,12 @@ class TerminalPage extends StatefulWidget { } class _TerminalPageState extends State - with AutomaticKeepAliveClientMixin { + with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { late FFI _ffi; late TerminalModel _terminalModel; + double? _cellHeight; + double _sysKeyboardHeight = 0; + Timer? _keyboardDebounce; // For web only. // 'monospace' does not work on web, use Google Fonts, `??` is only for null safety. @@ -44,6 +49,7 @@ class _TerminalPageState extends State @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); debugPrint( '[TerminalPage] Initializing terminal ${widget.terminalId} for peer ${widget.id}'); @@ -62,6 +68,10 @@ class _TerminalPageState extends State debugPrint( '[TerminalPage] Terminal model created for terminal ${widget.terminalId}'); + _terminalModel.onResizeExternal = (w, h, pw, ph) { + _cellHeight = ph * 1.0; + }; + // Register this terminal model with FFI for event routing _ffi.registerTerminalModel(widget.terminalId, _terminalModel); @@ -78,10 +88,36 @@ class _TerminalPageState extends State // Unregister terminal model from FFI _ffi.unregisterTerminalModel(widget.terminalId); _terminalModel.dispose(); + _keyboardDebounce?.cancel(); + WidgetsBinding.instance.removeObserver(this); super.dispose(); TerminalConnectionManager.releaseConnection(widget.id); } + @override + void didChangeMetrics() { + super.didChangeMetrics(); + + _keyboardDebounce?.cancel(); + _keyboardDebounce = Timer(const Duration(milliseconds: 20), () { + final bottomInset = MediaQuery.of(context).viewInsets.bottom; + setState(() { + _sysKeyboardHeight = bottomInset; + }); + }); + } + + EdgeInsets _calculatePadding(double heightPx) { + if (_cellHeight == null) { + return const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0); + } + final realHeight = heightPx - _sysKeyboardHeight; + final rows = (realHeight / _cellHeight!).floor(); + final extraSpace = realHeight - rows * _cellHeight!; + final topBottom = max(0.0, extraSpace / 2.0); + return EdgeInsets.only(left: 5.0, right: 5.0, top: topBottom, bottom: topBottom + _sysKeyboardHeight); + } + @override Widget build(BuildContext context) { super.build(context); @@ -96,28 +132,37 @@ class _TerminalPageState extends State Widget buildBody() { return Scaffold( + resizeToAvoidBottomInset: false, // Disable automatic layout adjustment; manually control UI updates to prevent flickering when the keyboard shows/hides backgroundColor: Theme.of(context).scaffoldBackgroundColor, - body: TerminalView( - _terminalModel.terminal, - controller: _terminalModel.terminalController, - autofocus: true, - textStyle: _getTerminalStyle(), - backgroundOpacity: 0.7, - padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0), - onSecondaryTapDown: (details, offset) async { - final selection = _terminalModel.terminalController.selection; - if (selection != null) { - final text = _terminalModel.terminal.buffer.getText(selection); - _terminalModel.terminalController.clearSelection(); - await Clipboard.setData(ClipboardData(text: text)); - } else { - final data = await Clipboard.getData('text/plain'); - final text = data?.text; - if (text != null) { - _terminalModel.terminal.paste(text); - } - } - }, + body: SafeArea( + top: true, + child: LayoutBuilder( + builder: (context, constraints) { + final heightPx = constraints.maxHeight; + return TerminalView( + _terminalModel.terminal, + controller: _terminalModel.terminalController, + autofocus: true, + textStyle: _getTerminalStyle(), + backgroundOpacity: 0.7, + padding: _calculatePadding(heightPx), + onSecondaryTapDown: (details, offset) async { + final selection = _terminalModel.terminalController.selection; + if (selection != null) { + final text = _terminalModel.terminal.buffer.getText(selection); + _terminalModel.terminalController.clearSelection(); + await Clipboard.setData(ClipboardData(text: text)); + } else { + final data = await Clipboard.getData('text/plain'); + final text = data?.text; + if (text != null) { + _terminalModel.terminal.paste(text); + } + } + }, + ); + }, + ), ), ); }