didUpdateWidget method

  1. @override
void didUpdateWidget(
  1. covariant TerminalView oldWidget
)
override

Called whenever the widget configuration changes.

If the parent widget rebuilds and requests that this location in the tree update to display a new widget with the same runtimeType and Widget.key, the framework will update the widget property of this State object to refer to the new widget and then call this method with the previous widget as an argument.

Override this method to respond when the widget changes (e.g., to start implicit animations).

The framework always calls build after calling didUpdateWidget, which means any calls to setState in didUpdateWidget are redundant.

If a State's build method depends on an object that can itself change state, for example a ChangeNotifier or Stream, or some other object to which one can subscribe to receive notifications, then be sure to subscribe and unsubscribe properly in initState, didUpdateWidget, and dispose:

  • In initState, subscribe to the object.
  • In didUpdateWidget unsubscribe from the old object and subscribe to the new one if the updated widget configuration requires replacing the object.
  • In dispose, unsubscribe from the object.

Implementations of this method should start with a call to the inherited method, as in super.didUpdateWidget(oldWidget).

See the discussion at Element.rebuild for more information on when this method is called.

Implementation

@override
void didUpdateWidget(covariant TerminalView oldWidget) {
  super.didUpdateWidget(oldWidget);
  // Engine swap: the host replaced the engine (e.g. ExampleTerminalApp
  // restart after shell exit). Cancel the bell subscription pointing at
  // the OLD engine — whose stream controller has already been closed by
  // `engine.dispose()` — and re-subscribe to the new engine's bell. Without
  // this, post-restart Bell events flow into a dead stream and the visual
  // / audible bell silently breaks. Pinned by the regression test
  // `bell after engine swap still flashes`.
  if (!identical(widget.engine, oldWidget.engine)) {
    oldWidget.engine.onCancelCoalescedScroll = null;
    _bellSub?.cancel();
    _bellSub = widget.engine.bell.listen((_) => _flashBell());
    _lastReportedCaretRect = null;
    // Re-wire grid listener to the new engine's grid.
    if (oldWidget.linkProviders.isNotEmpty) {
      oldWidget.engine.gridForView.removeListener(_onGridLinkContextChanged);
    }
    if (widget.linkProviders.isNotEmpty) {
      widget.engine.gridForView.addListener(_onGridLinkContextChanged);
      _lastDisplayOffset = widget.engine.gridForView.displayOffset;
      _lastScrollFraction = widget.engine.gridForView.scrollFraction;
    }
    _hoverLinkRow = null;
    _linkOverlay = LinkOverlay.empty;
    // The view is reused across engine swaps (TeamPilot host swaps engines
    // per member/tab). Abandon any in-flight IME pre-edit so a stale
    // composition can't commit into the swapped-in engine. notify:false +
    // direct field assignment because build() runs right after didUpdateWidget
    // (calling setState here would throw).
    if (_ime.isComposing || _preedit != null) {
      _ime.resetComposing(notify: false);
      _preedit = null;
    }
    // Reused view: a kinetic fling coasting at swap time would scroll the
    // swapped-in engine. Rebuild the scroll controller so PTY writes and
    // history scroll target the swapped-in engine (same pattern as resize).
    _scrollController.dispose();
    _scrollController = _newScrollController();
    _wireCoalescedScrollCancel();
    _scrollController.onGestureStart();
    _viewportController.dispose();
    _viewportController = TerminalViewportController(
      engine: widget.engine,
      onPtyResize: widget.onPtyResize,
    );
  }
  if (widget.onPtyResize != oldWidget.onPtyResize) {
    _viewportController.updateHostPtyResize(widget.onPtyResize);
  }
  // Re-attach controller / focus if the caller swapped them out. Rare; the
  // host typically creates both once.
  if (!identical(widget.controller, oldWidget.controller)) {
    if (_ownsController) _controller.dispose();
    _controller = widget.controller ?? (TerminalController()..attach(_engine));
    _ownsController = widget.controller == null;
  }
  if (!identical(widget.focusNode, oldWidget.focusNode)) {
    // Always remove our listeners from the old node regardless of ownership
    // — otherwise a host-supplied node that gets swapped out retains our
    // listeners forever (memory leak + spurious callbacks against the
    // disposed view).
    _focus.removeListener(_reportFocus);
    _focus.removeListener(_handleImeFocusChange);
    if (_ownsFocus) _focus.dispose();
    _focus = widget.focusNode ?? FocusNode();
    _ownsFocus = widget.focusNode == null;
    _focus.addListener(_reportFocus);
    _focus.addListener(_handleImeFocusChange);
  }
  if (oldWidget.bellDuration != widget.bellDuration) {
    _bellCtrl.duration = widget.bellDuration > Duration.zero
        ? widget.bellDuration
        : const Duration(milliseconds: 1);
  }
  if (oldWidget.cursorBlinkInterval != widget.cursorBlinkInterval) {
    _blinkTimer?.cancel();
    _blinkTimer =
        Timer.periodic(widget.cursorBlinkInterval, (_) => _blinkTick());
  }
  if (oldWidget.textStyle != widget.textStyle) {
    setState(() {
      _fontSize = widget.textStyle.size;
      _glyphs.dispose();
      _style = widget.textStyle.toTextStyle().copyWith(fontSize: _fontSize);
      _metrics = CellMetrics.measure(_style);
      _scrollController.updateCellHeight(_metrics.height);
      widget.engine.setCellPixels(
          _metrics.width.round(), _metrics.height.round());
      _glyphs = _newGlyphCache();
      _disposeAtlas(); // rebuilt lazily in build() with the new metrics
    });
  }
  if (!identical(widget.linkProviders, oldWidget.linkProviders)) {
    for (final p in oldWidget.linkProviders) {
      p.removeListener(_onProviderChanged);
    }
    for (final p in widget.linkProviders) {
      p.addListener(_onProviderChanged);
    }
    if (oldWidget.linkProviders.isNotEmpty) {
      _grid.removeListener(_onGridLinkContextChanged);
    }
    if (widget.linkProviders.isNotEmpty) {
      _lastDisplayOffset = _grid.displayOffset;
      _lastScrollFraction = _grid.scrollFraction;
      _grid.addListener(_onGridLinkContextChanged);
    } else {
      _hoverLinkRow = null;
      if (_linkOverlay != LinkOverlay.empty) {
        setState(() => _linkOverlay = LinkOverlay.empty);
      }
    }
    _refreshHoverLinkOverlay();
  }
}