chromium/components/remote_cocoa/app_shim/native_widget_ns_window_fullscreen_controller.mm

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/remote_cocoa/app_shim/native_widget_ns_window_fullscreen_controller.h"

#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#import "base/task/single_thread_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "ui/base/cocoa/nswindow_test_util.h"

namespace remote_cocoa {

namespace {

bool IsFakeForTesting() {
  return ui::NSWindowFakedForTesting::IsEnabled();
}

}  // namespace

NativeWidgetNSWindowFullscreenController::
    NativeWidgetNSWindowFullscreenController(Client* client)
    : client_(client) {}

NativeWidgetNSWindowFullscreenController::
    ~NativeWidgetNSWindowFullscreenController() {}

void NativeWidgetNSWindowFullscreenController::EnterFullscreen(
    int64_t target_display_id) {
  if (IsFakeForTesting()) {
    if (state_ == State::kWindowed) {
      state_ = State::kEnterFullscreenTransition;
      client_->FullscreenControllerTransitionStart(true);

      windowed_frame_ = client_->FullscreenControllerGetFrame();
      const gfx::Rect kFakeFullscreenRect(0, 0, 1024, 768);
      client_->FullscreenControllerSetFrame(kFakeFullscreenRect,
                                            /*animate=*/false,
                                            base::DoNothing());
      state_ = State::kFullscreen;
      client_->FullscreenControllerTransitionComplete(true);
    }
    return;
  }

  if (state_ == State::kFullscreen) {
    // Early-out for no-ops.
    if (target_display_id == display::kInvalidDisplayId ||
        target_display_id == client_->FullscreenControllerGetDisplayId()) {
      return;
    }
  } else if (state_ == State::kWindowed) {
    windowed_frame_ = client_->FullscreenControllerGetFrame();
  }

  // If we are starting a new transition, then notify `client_`.
  if (!IsInFullscreenTransition()) {
    client_->FullscreenControllerTransitionStart(true);
  }

  pending_state_ = PendingState();
  pending_state_->is_fullscreen = true;
  pending_state_->display_id = target_display_id;
  HandlePendingState();
  DCHECK(IsInFullscreenTransition());
}

void NativeWidgetNSWindowFullscreenController::ExitFullscreen() {
  if (IsFakeForTesting()) {
    if (state_ == State::kFullscreen) {
      state_ = State::kExitFullscreenTransition;
      client_->FullscreenControllerTransitionStart(false);
      client_->FullscreenControllerSetFrame(windowed_frame_.value(),
                                            /*animate=*/false,
                                            base::DoNothing());
      state_ = State::kWindowed;
      client_->FullscreenControllerTransitionComplete(false);
    }
    return;
  }

  // Early-out for no-ops.
  if (state_ == State::kWindowed)
    return;

  // If we are starting a new transition, then notify `client_`.
  if (!IsInFullscreenTransition())
    client_->FullscreenControllerTransitionStart(false);

  pending_state_ = PendingState();
  pending_state_->is_fullscreen = false;
  HandlePendingState();
  DCHECK(IsInFullscreenTransition());
}

void NativeWidgetNSWindowFullscreenController::
    MoveToTargetDisplayThenToggleFullscreen(int64_t target_display_id) {
  DCHECK_EQ(state_, State::kWindowedMovingToFullscreenTarget);

  gfx::Rect display_frame =
      client_->FullscreenControllerGetFrameForDisplay(target_display_id);
  if (!display_frame.IsEmpty()) {
    DCHECK(windowed_frame_);
    restore_windowed_frame_ = true;
    SetStateAndCancelPostedTasks(State::kEnterFullscreenTransition);
    client_->FullscreenControllerSetFrame(
        display_frame, /*animate=*/true,
        base::BindOnce(
            &NativeWidgetNSWindowFullscreenController::ToggleFullscreen,
            weak_factory_.GetWeakPtr()));
  }
}

void NativeWidgetNSWindowFullscreenController::RestoreWindowedFrame() {
  DCHECK_EQ(state_, State::kWindowedRestoringOriginalFrame);
  DCHECK(restore_windowed_frame_);
  DCHECK(windowed_frame_);
  client_->FullscreenControllerSetFrame(
      windowed_frame_.value(),
      /*animate=*/true,
      base::BindOnce(
          &NativeWidgetNSWindowFullscreenController::OnWindowedFrameRestored,
          weak_factory_.GetWeakPtr()));
}

void NativeWidgetNSWindowFullscreenController::OnWindowedFrameRestored() {
  restore_windowed_frame_ = false;

  SetStateAndCancelPostedTasks(State::kWindowed);
  HandlePendingState();
  if (!IsInFullscreenTransition()) {
    client_->FullscreenControllerTransitionComplete(
        /*is_fullscreen=*/false);
  }
}

void NativeWidgetNSWindowFullscreenController::ToggleFullscreen() {
  // Note that OnWindowWillEnterFullscreen or OnWindowWillExitFullscreen will
  // be called within the below call.
  client_->FullscreenControllerToggleFullscreen();
}

bool NativeWidgetNSWindowFullscreenController::CanResize() const {
  // Don't modify the size constraints or fullscreen collection behavior while
  // in fullscreen or during a transition. OnFullscreenTransitionComplete will
  // reset these after leaving fullscreen.
  return state_ == State::kWindowed;
}

void NativeWidgetNSWindowFullscreenController::SetStateAndCancelPostedTasks(
    State new_state) {
  weak_factory_.InvalidateWeakPtrs();
  state_ = new_state;
}

void NativeWidgetNSWindowFullscreenController::OnWindowWantsToClose() {
  if (state_ == State::kEnterFullscreenTransition ||
      state_ == State::kExitFullscreenTransition) {
    has_deferred_window_close_ = true;
  }
}

void NativeWidgetNSWindowFullscreenController::OnWindowWillClose() {
  // If a window closes while in a fullscreen transition, then the window will
  // hang in a zombie-like state.
  // https://crbug.com/945237
  if (state_ != State::kWindowed && state_ != State::kFullscreen) {
    DLOG(ERROR) << "-[NSWindow close] while in fullscreen transition will "
                   "trigger zombie windows.";
  }
}

void NativeWidgetNSWindowFullscreenController::OnWindowWillEnterFullscreen() {
  if (state_ == State::kWindowed) {
    windowed_frame_ = client_->FullscreenControllerGetFrame();
  }

  // If we are starting a new transition, then notify `client_`.
  if (!IsInFullscreenTransition()) {
    client_->FullscreenControllerTransitionStart(true);
  }

  SetStateAndCancelPostedTasks(State::kEnterFullscreenTransition);
  DCHECK(IsInFullscreenTransition());
}

void NativeWidgetNSWindowFullscreenController::OnWindowDidEnterFullscreen() {
  if (HandleDeferredClose())
    return;
  if (state_ == State::kExitFullscreenTransition) {
    // If transitioning out of fullscreen failed, then just remain in
    // fullscreen. Note that `pending_state_` could have been left present for a
    // fullscreen-to-fullscreen-on-another-display transition. If it looks like
    // we are in that situation, reset `pending_state_`.
    if (pending_state_ && pending_state_->is_fullscreen)
      pending_state_.reset();
  }
  SetStateAndCancelPostedTasks(State::kFullscreen);
  HandlePendingState();
  if (!IsInFullscreenTransition()) {
    client_->FullscreenControllerTransitionComplete(
        /*target_fullscreen_state=*/true);
  }
}

void NativeWidgetNSWindowFullscreenController::OnWindowWillExitFullscreen() {
  // If we are starting a new transition, then notify `client_`.
  if (!IsInFullscreenTransition())
    client_->FullscreenControllerTransitionStart(false);

  SetStateAndCancelPostedTasks(State::kExitFullscreenTransition);
  DCHECK(IsInFullscreenTransition());
}

void NativeWidgetNSWindowFullscreenController::OnWindowDidExitFullscreen() {
  if (HandleDeferredClose())
    return;
  SetStateAndCancelPostedTasks(State::kWindowed);
  HandlePendingState();
  if (!IsInFullscreenTransition()) {
    client_->FullscreenControllerTransitionComplete(
        /*is_fullscreen=*/false);
  }
}

void NativeWidgetNSWindowFullscreenController::HandlePendingState() {
  // If in kWindowed or kFullscreen, then consume `pending_state_`.
  switch (state_) {
    case State::kClosed:
      pending_state_.reset();
      return;
    case State::kWindowed:
      if (pending_state_ && pending_state_->is_fullscreen) {
        if (pending_state_->display_id != display::kInvalidDisplayId &&
            pending_state_->display_id !=
                client_->FullscreenControllerGetDisplayId()) {
          // Handle entering fullscreen on another specified display.
          SetStateAndCancelPostedTasks(
              State::kWindowedMovingToFullscreenTarget);
          base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
              FROM_HERE,
              base::BindOnce(&NativeWidgetNSWindowFullscreenController::
                                 MoveToTargetDisplayThenToggleFullscreen,
                             weak_factory_.GetWeakPtr(),
                             pending_state_->display_id));
        } else {
          // Handle entering fullscreen on the default display.
          SetStateAndCancelPostedTasks(State::kEnterFullscreenTransition);
          base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
              FROM_HERE,
              base::BindOnce(
                  &NativeWidgetNSWindowFullscreenController::ToggleFullscreen,
                  weak_factory_.GetWeakPtr()));
        }
      } else if (restore_windowed_frame_) {
        // Handle returning to the kWindowed state after having been fullscreen
        // and having called setFrame during some transition. It is necessary
        // to restore the original frame prior to having entered fullscreen.
        SetStateAndCancelPostedTasks(State::kWindowedRestoringOriginalFrame);
        base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
            FROM_HERE,
            base::BindOnce(
                &NativeWidgetNSWindowFullscreenController::RestoreWindowedFrame,
                weak_factory_.GetWeakPtr()));
      }
      // Always reset `pending_state_` when handling kWindowed state.
      pending_state_.reset();
      return;
    case State::kFullscreen:
      if (pending_state_) {
        if (pending_state_->is_fullscreen) {
          // If `pending_state_` is a no-op, then reset it.
          if (pending_state_->display_id == display::kInvalidDisplayId ||
              pending_state_->display_id ==
                  client_->FullscreenControllerGetDisplayId()) {
            pending_state_.reset();
            return;
          }
          // Leave `pending_state_` in place. It will be consumed when we
          // come through here again via OnWindowDidExitFullscreen (or
          // via OnWindowDidEnterFullscreen, if we fail to exit fullscreen).
        } else {
          pending_state_.reset();
        }
        SetStateAndCancelPostedTasks(State::kExitFullscreenTransition);
        base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
            FROM_HERE,
            base::BindOnce(
                &NativeWidgetNSWindowFullscreenController::ToggleFullscreen,
                weak_factory_.GetWeakPtr()));
      }
      return;
    default:
      // Leave `pending_state_` unchanged. It will be re-examined when our
      // transition completes.
      break;
  }
}

bool NativeWidgetNSWindowFullscreenController::HandleDeferredClose() {
  CHECK_NE(state_, State::kClosed);
  if (has_deferred_window_close_) {
    SetStateAndCancelPostedTasks(State::kClosed);
    // Note that `this` may be deleted by the below call.
    client_->FullscreenControllerCloseWindow();
    return true;
  }
  return false;
}

bool NativeWidgetNSWindowFullscreenController::GetTargetFullscreenState()
    const {
  if (pending_state_)
    return pending_state_->is_fullscreen;
  switch (state_) {
    case State::kWindowed:
    case State::kWindowedRestoringOriginalFrame:
    case State::kExitFullscreenTransition:
    case State::kClosed:
      return false;
    case State::kWindowedMovingToFullscreenTarget:
    case State::kEnterFullscreenTransition:
    case State::kFullscreen:
      return true;
  }
}

bool NativeWidgetNSWindowFullscreenController::IsInFullscreenTransition()
    const {
  switch (state_) {
    case State::kWindowed:
    case State::kFullscreen:
    case State::kClosed:
      return false;
    default:
      return true;
  }
}

}  // namespace remote_cocoa