{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import panel as pn\n",
    "import reacton\n",
    "import reacton.ipywidgets as w\n",
    "\n",
    "pn.extension('ipywidgets')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The ``Reacton`` pane renders [Reacton](https://reacton.solara.dev/en/latest/) components both in the notebook and in a deployed server. Reacton provides a way to write reusable components in a React-like way, to make Python-based UI's using the ipywidgets ecosystem (ipywidgets, ipyvolume, bqplot, threejs, leaflet, ipyvuetify, ...). Note that Reacton is primarily a way to write apps\n",
    "\n",
    "In the notebook this is not necessary since Panel simply uses the regular notebook ipywidget renderer. Particularly in JupyterLab importing the ipywidgets extension in this way may interfere with the UI and render the JupyterLab UI unusable, so enable the extension with care.\n",
    "\n",
    "#### Parameters:\n",
    "\n",
    "For details on other options for customizing the component see the [layout](../../how_to/layout/index.md) and [styling](../../how_to/styling/index.md) how-to guides.\n",
    "\n",
    "* **``object``** (object): The ipywidget object being displayed\n",
    "\n",
    "##### Display\n",
    "\n",
    "* **``default_layout``** (pn.layout.Panel, default=Row): Layout to wrap the plot and widgets in\n",
    "\n",
    "___"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The `panel` function will automatically convert any ``ipywidgets`` object into a displayable panel, while keeping all of its interactive features:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "@reacton.component\n",
    "def ButtonClick():\n",
    "    # first render, this return 0, after that, the last argument\n",
    "    # of set_clicks\n",
    "    clicks, set_clicks = reacton.use_state(0)\n",
    "    \n",
    "    def my_click_handler():\n",
    "        # trigger a new render with a new value for clicks\n",
    "        set_clicks(clicks+1)\n",
    "\n",
    "    button = w.Button(description=f\"Clicked {clicks} times\",\n",
    "                      on_click=my_click_handler)\n",
    "    return button\n",
    "\n",
    "pn.panel(ButtonClick())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Combining Reacton and Panel components\n",
    "\n",
    "Reacton can be used in conjunction with Panel components however we have to make two modifications:\n",
    "\n",
    "1. Panel components have to be wrapped as an ipywidget using the `pn.ipywidget` wrapper (this requires `jupyter_bokeh`).\n",
    "2. The wrapped Panel component must be added to a reacton layout component.\n",
    "\n",
    "In the example below we swap out the `reacton.ipywidgets.Button` for a `pn.widgets.Button` and then wrap it in `pn.ipywidgets` and a `reacton.ipywidgets.VBox`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "@reacton.component\n",
    "def PanelButtonClick():\n",
    "    # first render, this return 0, after that, the last argument\n",
    "    # of set_clicks\n",
    "    clicks, set_clicks = reacton.use_state(0)\n",
    "    \n",
    "    def my_click_handler(event):\n",
    "        # trigger a new render with a new value for clicks\n",
    "        set_clicks(clicks+1)\n",
    "\n",
    "    button = pn.widgets.Button(name=f'Clicked {clicks} times')\n",
    "    button.on_click(my_click_handler)\n",
    "\n",
    "    return w.VBox(children=[pn.ipywidget(button)])\n",
    "\n",
    "pn.panel(PanelButtonClick(), height=50)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Complex examples\n",
    "\n",
    "Even more complex applications can be built in Reacton and displayed in Panel. Here is a Calculator example from the Reacton documentation.\n",
    "\n",
    "### Logic"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import ast\n",
    "import dataclasses\n",
    "import operator\n",
    "from typing import Any, Optional\n",
    "\n",
    "DEBUG = False\n",
    "operator_map = {\n",
    "    \"x\": operator.mul,\n",
    "    \"/\": operator.truediv,\n",
    "    \"+\": operator.add,\n",
    "    \"-\": operator.sub,\n",
    "}\n",
    "\n",
    "@dataclasses.dataclass(frozen=True)\n",
    "class CalculatorState:\n",
    "    input: str = \"\"\n",
    "    output: str = \"\"\n",
    "    left: float = 0\n",
    "    right: Optional[float] = None\n",
    "    operator: Any = operator.add\n",
    "    error: str = \"\"\n",
    "\n",
    "\n",
    "initial_state = CalculatorState()\n",
    "\n",
    "\n",
    "def calculate(state: CalculatorState):\n",
    "    result = state.operator(state.left, state.right)\n",
    "    return dataclasses.replace(state, left=result)\n",
    "\n",
    "\n",
    "def calculator_reducer(state: CalculatorState, action):\n",
    "    action_type, payload = action\n",
    "    if DEBUG:\n",
    "        print(\"reducer\", state, action_type, payload)  # noqa\n",
    "    state = dataclasses.replace(state, error=\"\")\n",
    "\n",
    "    if action_type == \"digit\":\n",
    "        digit = payload\n",
    "        input = state.input + digit\n",
    "        return dataclasses.replace(state, input=input, output=input)\n",
    "    elif action_type == \"percent\":\n",
    "        if state.input:\n",
    "            try:\n",
    "                value = ast.literal_eval(state.input)\n",
    "            except Exception as e:\n",
    "                return dataclasses.replace(state, error=str(e))\n",
    "            state = dataclasses.replace(state, right=value / 100)\n",
    "            state = calculate(state)\n",
    "            output = f\"{value / 100:,}\"\n",
    "            return dataclasses.replace(state, output=output, input=\"\")\n",
    "        else:\n",
    "            output = f\"{state.left / 100:,}\"\n",
    "            return dataclasses.replace(state, left=state.left / 100, output=output)\n",
    "    elif action_type == \"negate\":\n",
    "        if state.input:\n",
    "            input = state.output\n",
    "            input = input[1:] if input[0] == \"-\" else \"-\" + input\n",
    "            output = input\n",
    "            return dataclasses.replace(state, input=input, output=output)\n",
    "        else:\n",
    "            output = f\"{-state.left:,}\"\n",
    "            return dataclasses.replace(state, left=-state.left, output=output)\n",
    "    elif action_type == \"clear\":\n",
    "        return dataclasses.replace(state, input=\"\", output=\"\")\n",
    "    elif action_type == \"reset\":\n",
    "        return initial_state\n",
    "    elif action_type == \"calculate\":\n",
    "        if state.input:\n",
    "            try:\n",
    "                value = ast.literal_eval(state.input)\n",
    "            except Exception as e:\n",
    "                return dataclasses.replace(state, error=str(e))\n",
    "            state = dataclasses.replace(state, right=value)\n",
    "        state = calculate(state)\n",
    "        output = f\"{state.left:,}\"\n",
    "        state = dataclasses.replace(state, output=output, input=\"\")\n",
    "        return state\n",
    "    elif action_type == \"operator\":\n",
    "        if state.input:\n",
    "            state = calculator_reducer(state, (\"calculate\", None))\n",
    "            state = dataclasses.replace(state, operator=payload, input=\"\")\n",
    "        else:\n",
    "            # e.g. 2+3=*= should give 5,25\n",
    "            state = dataclasses.replace(state, operator=payload, right=state.left)\n",
    "        return state\n",
    "    else:\n",
    "        print(\"invalid action\", action)  # noqa\n",
    "        return state\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### UI\n",
    "\n",
    "#### ipywidgets"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "@reacton.component\n",
    "def Calculator():\n",
    "    state, dispatch = reacton.use_reducer(calculator_reducer, initial_state)\n",
    "    with w.VBox() as main:\n",
    "        w.HTML(value=\"<b>Calculator Using Reacton</b>\")\n",
    "        with w.VBox():\n",
    "            w.HTML(value=state.error or state.output or \"0\")\n",
    "            with w.HBox():\n",
    "                if state.input:\n",
    "                    w.Button(description=\"C\", on_click=lambda: dispatch((\"clear\", None)))\n",
    "                else:\n",
    "                    w.Button(description=\"AC\", on_click=lambda: dispatch((\"reset\", None)))\n",
    "                w.Button(description=\"+/-\", on_click=lambda: dispatch((\"negate\", None)))\n",
    "                w.Button(description=\"%\", on_click=lambda: dispatch((\"percent\", None)))\n",
    "                w.Button(description=\"/\", on_click=lambda: dispatch((\"operator\", operator_map[\"/\"])))\n",
    "\n",
    "            column_op = [\"x\", \"-\", \"+\"]\n",
    "            for i in range(3):\n",
    "                with w.HBox():\n",
    "                    for j in range(3):\n",
    "                        digit = str(j + (2 - i) * 3 + 1)\n",
    "                        w.Button(description=digit, on_click=lambda digit=digit: dispatch((\"digit\", digit)))\n",
    "                    op_symbol = column_op[i]\n",
    "                    op = operator_map[op_symbol]\n",
    "                    w.Button(description=op_symbol, on_click=lambda op=op: dispatch((\"operator\", op)))\n",
    "            with w.HBox():\n",
    "                def boom():\n",
    "                    print(\"boom\")\n",
    "                    raise ValueError(\"boom\")\n",
    "\n",
    "                w.Button(description=\"?\", on_click=boom)\n",
    "\n",
    "                w.Button(description=\"0\", on_click=lambda: dispatch((\"digit\", \"0\")))\n",
    "                w.Button(description=\".\", on_click=lambda: dispatch((\"digit\", \".\")))\n",
    "\n",
    "                w.Button(description=\"=\", on_click=lambda: dispatch((\"calculate\", None)))\n",
    "\n",
    "    return main\n",
    "\n",
    "calculator = Calculator()\n",
    "    \n",
    "pn.pane.Reacton(calculator, width=500, height=250)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### ipyvuetify"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import reacton.ipyvuetify as v\n",
    "\n",
    "@reacton.component\n",
    "def CalculatorVuetify():\n",
    "    state, dispatch = reacton.use_reducer(calculator_reducer, initial_state)\n",
    "    with v.Card(elevation=10, class_=\"ma-4\") as main:\n",
    "        with v.CardTitle(children=[\"Calculator\"]):\n",
    "            pass\n",
    "        with v.CardSubtitle(children=[\"With ipyvuetify and Reacton\"]):\n",
    "            pass\n",
    "        with v.CardText():\n",
    "            with w.VBox():\n",
    "                w.HTML(value=state.error or state.output or \"0\")\n",
    "                class_ = \"pa-0 ma-1\"\n",
    "\n",
    "                with w.HBox():\n",
    "                    if state.input:\n",
    "                        v.BtnWithClick(children=\"C\", on_click=lambda: dispatch((\"clear\", None)), dark=True, class_=class_)\n",
    "                    else:\n",
    "                        v.BtnWithClick(children=\"AC\", on_click=lambda: dispatch((\"reset\", None)), dark=True, class_=class_)\n",
    "                    v.BtnWithClick(children=\"+/-\", on_click=lambda: dispatch((\"negate\", None)), dark=True, class_=class_)\n",
    "                    v.BtnWithClick(children=\"%\", on_click=lambda: dispatch((\"percent\", None)), dark=True, class_=class_)\n",
    "                    v.BtnWithClick(children=\"/\", color=\"primary\", on_click=lambda: dispatch((\"operator\", operator_map[\"/\"])), class_=class_)\n",
    "\n",
    "                column_op = [\"x\", \"-\", \"+\"]\n",
    "                for i in range(3):\n",
    "                    with w.HBox():\n",
    "                        for j in range(3):\n",
    "                            digit = str(j + (2 - i) * 3 + 1)\n",
    "                            v.BtnWithClick(children=digit, on_click=lambda digit=digit: dispatch((\"digit\", digit)), class_=class_)\n",
    "                        op_symbol = column_op[i]\n",
    "                        op = operator_map[op_symbol]\n",
    "                        v.BtnWithClick(children=op_symbol, color=\"primary\", on_click=lambda op=op: dispatch((\"operator\", op)), class_=class_)\n",
    "                with w.HBox():\n",
    "                    def boom():\n",
    "                        print(\"boom\")\n",
    "                        raise ValueError(\"boom\")\n",
    "\n",
    "                    v.BtnWithClick(children=\"?\", on_click=boom, class_=class_)\n",
    "\n",
    "                    v.BtnWithClick(children=\"0\", on_click=lambda: dispatch((\"digit\", \"0\")), class_=class_)\n",
    "                    v.BtnWithClick(children=\".\", on_click=lambda: dispatch((\"digit\", \".\")), class_=class_)\n",
    "\n",
    "                    v.BtnWithClick(children=\"=\", color=\"primary\", on_click=lambda: dispatch((\"calculate\", None)), class_=class_)\n",
    "\n",
    "    return main\n",
    "\n",
    "pn.pane.Reacton(CalculatorVuetify(), width=500, height=420)"
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
