lib/evision_wx.ex

defmodule Evision.Wx do
  @moduledoc """
  Interact with wxWidgets
  """

  # use GenServer
  @behaviour :wx_object

  @wxVERTICAL 8
  @wxEXPAND 8192
  @wxFULL_REPAINT_ON_RESIZE 65536
  @process_env_key "Evision.Wx.windows"

  defp check_wx! do
    case Code.ensure_compiled(:wx) do
      {:error, _} ->
        raise RuntimeError,
              ":wx is not available, please ensure Erlang was compiled with wxWidgets."

      _ ->
        :ok
    end
  end

  @spec imshow(String.t(), Evision.Mat.maybe_mat_in()) :: :ok
  def imshow(window_name, image) when is_binary(window_name) and is_struct(image, Evision.Mat) do
    check_wx!()

    windows = Process.get(@process_env_key, %{})
    window = Map.get(windows, window_name)

    window =
      if window == nil do
        wx_window = :wx.new()

        wx_obj =
          {:wx_ref, _, :wxFrame, window} =
          :wx_object.start(__MODULE__, {window_name, wx_window}, [])

        Process.put(@process_env_key, Map.put(windows, window_name, wx_obj))
        window
      else
        {:wx_ref, _, :wxFrame, window} = window
        window
      end

    GenServer.call(window, {:imshow, image})
  end

  @spec destroyWindow(String.t()) :: :ok
  def destroyWindow(window_name) do
    check_wx!()

    windows = Process.get(@process_env_key, %{})
    window = Map.get(windows, window_name)

    if window do
      :wxFrame.close(window)
      Process.put(@process_env_key, Map.delete(windows, window_name))
    end

    :ok
  end

  @spec destroyAllWindows() :: :ok
  def destroyAllWindows() do
    check_wx!()

    windows = Process.get(@process_env_key, %{})

    Enum.each(Map.values(windows), fn window ->
      :wxFrame.close(window)
    end)

    Process.put(@process_env_key, %{})
    :ok
  end

  @impl true
  def init({window_name, window}) do
    :wx.batch(fn ->
      frame = :wxFrame.new(window, 0, String.to_charlist(window_name))
      panel = :wxPanel.new(frame, [])

      mainSizer = :wxBoxSizer.new(@wxVERTICAL)
      sizer = :wxStaticBoxSizer.new(@wxVERTICAL, panel)

      canvas = :wxPanel.new(panel, style: @wxFULL_REPAINT_ON_RESIZE)

      :wxPanel.connect(canvas, :paint, [:callback])
      :wxPanel.connect(canvas, :size)
      :wxPanel.connect(canvas, :left_down)
      :wxPanel.connect(canvas, :left_up)
      :wxPanel.connect(canvas, :motion)

      :wxSizer.add(sizer, canvas, flag: @wxEXPAND, proportion: 1)
      :wxSizer.add(mainSizer, sizer, flag: @wxEXPAND, proportion: 1)
      :wxPanel.setSizer(panel, mainSizer)
      :wxSizer.layout(mainSizer)

      {w, h} = :wxPanel.getSize(canvas)
      bitmap = :wxBitmap.new(max(w, 30), max(h, 30))
      :wxFrame.show(frame)

      {frame,
       %{
         window_name: window_name,
         window: window,
         frame: frame,
         panel: panel,
         canvas: canvas,
         bitmap: bitmap,
         image: nil
       }}
    end)
  end

  @impl true
  def handle_event(
        {:wx, _, _, _, {:wxSize, :size, {w, h}, _}},
        state = %{canvas: canvas, bitmap: prev, image: image}
      ) do
    if w > 0 and h > 0 do
      bitmap = :wxBitmap.new(w, h)
      draw_canvas(image, canvas, bitmap)
      :wxBitmap.destroy(prev)
      {:noreply, %{state | bitmap: bitmap}}
    else
      {:noreply, state}
    end
  end

  @impl true
  def handle_event(_wx, state) do
    {:noreply, state}
  end

  @impl true
  def handle_sync_event({:wx, _, _, _, {:wxPaint, :paint}}, _obj, %{
        canvas: canvas,
        bitmap: bitmap,
        image: image
      }) do
    draw_canvas(image, canvas, bitmap)
    :ok
  end

  @impl true
  def handle_sync_event(_wx, _obj, _state) do
    :ok
  end

  @impl true
  def handle_call({:imshow, image}, _from, state = %{canvas: canvas, bitmap: bitmap}) do
    case draw_canvas(image, canvas, bitmap) do
      :ok ->
        {:reply, :ok, %{state | image: image}}

      :error ->
        {:reply, :error, state}
    end
  end

  defp draw_canvas(nil, _canvas, _bitmap) do
    :ok
  end

  defp draw_canvas(image, canvas, bitmap) when is_struct(image, Evision.Mat) do
    {canvasW, canvasH} = :wxPanel.getSize(canvas)
    canvas_w = trunc(canvasW)
    canvas_h = trunc(canvasH)

    {image_h, image_w} =
      case Evision.Mat.shape(image) do
        {h, w} ->
          {h, w}

        {h, w, c} when c in [1, 3, 4] ->
          {h, w}

        _ ->
          {0, 0}
      end

    with true <- image_h > 0 and image_w > 0 and canvas_h > 0 and canvas_w > 0,
         proportional = min(canvas_w / image_w, canvas_h / image_h),
         {target_w, target_h} = {trunc(image_w * proportional), trunc(image_h * proportional)},
         resized = Evision.resize(image, {target_w, target_h}),
         true <- is_struct(resized, Evision.Mat),
         rgb = Evision.cvtColor(resized, Evision.cv_COLOR_BGR2RGB()),
         true <- is_struct(rgb, Evision.Mat),
         binary = Evision.Mat.to_binary(rgb),
         true <- is_binary(binary) do
      img = :wxImage.new(target_w, target_h, binary)
      bmp = :wxBitmap.new(img)

      memoryDC = :wxMemoryDC.new(bitmap)
      :wxDC.clear(memoryDC)
      :wxDC.drawBitmap(memoryDC, bmp, {0, 0})
      canvasDC = :wxWindowDC.new(canvas)

      :wxDC.blit(
        canvasDC,
        {0, 0},
        {:wxBitmap.getWidth(bitmap), :wxBitmap.getHeight(bitmap)},
        memoryDC,
        {0, 0}
      )

      :wxWindowDC.destroy(canvasDC)
      :wxMemoryDC.destroy(memoryDC)

      :wxBitmap.destroy(bmp)
      :wxImage.destroy(img)
      :ok
    else
      _ ->
        :error
    end
  end

  defp draw_canvas(_, _canvas, _bitmap) do
    :ok
  end
end