{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "11b5c0b9",
   "metadata": {},
   "source": [
    "# Widgets\n",
    "\n",
    "Self-contained, interactive and controllable components.\n",
    "\n",
    "## What’s a widget?\n",
    "\n",
    "In Pret, a *widget* is a component that manages its own UI state, exposes a small imperative API (a “handle”) for commands like *scroll to X* or *focus*, and emits events (callbacks) when the user interacts with it. A widget runs on its own, without needing to be controlled by a parent component.\n",
    "\n",
    "In dashboards and multi-pane tools, panes often look at the same data from different angles but don’t need to mirror each other’s UI details (scroll positions, hovers, transient filters, typing...): this is where widgets shine, as they encapsulate their own UI state and logic.\n",
    "\n",
    "By contrast, a classic (controlled / presentational) component, such as [AnnotatedText][metanno.ui.AnnotatedText], keeps minimal or no state and expects its parent to hold all the UI logic and state and to pass it down as params.\n",
    "\n",
    "## If it looks like a widget and quacks like a widget...\n",
    "\n",
    "Widgets are not a formal class or type in Pret. They are just components that can run without being controlled by a parent. Just like any other component in Pret, widgets can be declared from inside a `@component` function (i.e., controlled by another component) or directly from your notebook/server. To interact with other parts of the UI, widgets expose two kinds of interfaces:\n",
    "\n",
    "#### 1) Events (callbacks)\n",
    "\n",
    "- Notify when an event occurs: `on_change(id)`, `on_hover(id, ...)`, ...\n",
    "- These are callbacks: they don’t change other widgets directly.\n",
    "\n",
    "#### 2) Imperative handle (commands)\n",
    "\n",
    "- Let other parts of the UI command this widget: `handle.current.scroll_to_id(id)`, `handle.current.set_active(id)`, `handle.current.set_filter(key, value)`, ...\n",
    "- These are UI actions and read-only getters like `handle.current.get_selection()`.\n",
    "\n",
    "!!! tip \"Actions object\"\n",
    "\n",
    "    In Pret, a common pattern is to pass a mutable `handle` Ref defined with [use_ref][pret.hooks.use_ref] that the widget fills: it writes callable entries under a `current` attribute, which the parent can then call imperatively.\n",
    "\n",
    "## Do's and don'ts\n",
    "\n",
    "- :no_entry_sign: Don’t wrap them in a stateful parent that is going to re-render often.\n",
    "- :no_entry_sign: Don't make them expect props that change with the app state (e.g., `value`).\n",
    "- :white_check_mark: You can compose them in a layout/panel/div for presentational purposes.\n",
    "- :white_check_mark: You can control them from the notebook or from other widgets through their `handle`.\n",
    "\n",
    "## Example\n",
    "\n",
    "Our objective is to define a counter and a log that stand alone, and from the notebook decide if and how they talk to each other, without introducing a new \"controller\" (or \"App\") component. Controlled components (ie, non widgets components) push you to create a shared parent to pass props around. Widgets keep their own state, expose a small actions handle, and emit events so you can wire them together (or not) from the notebook.\n",
    "\n",
    "Let's create a `CounterWidget` that exposes `reset` / `set` commands and an `on_change` event, and a `LogWidget` that exposes an `add` command.\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "e4048437",
   "metadata": {},
   "outputs": [],
   "source": [
    "from pret_joy import Button, Stack, Typography\n",
    "\n",
    "from pret import component\n",
    "from pret.hooks import use_event_callback, use_imperative_handle, use_ref, use_state\n",
    "\n",
    "\n",
    "@component\n",
    "def CounterWidget(handle=None, on_change=None):\n",
    "    count, set_count = use_state(0)\n",
    "\n",
    "    use_imperative_handle(\n",
    "        handle,\n",
    "        lambda: {\n",
    "            \"reset\": lambda: set_count(0),\n",
    "            \"set\": set_count,\n",
    "        },\n",
    "        [],\n",
    "    )\n",
    "\n",
    "    @use_event_callback\n",
    "    def increment():\n",
    "        def update(prev):\n",
    "            new_value = prev + 1\n",
    "            if on_change is not None:\n",
    "                on_change(new_value)\n",
    "            return new_value\n",
    "\n",
    "        set_count(update)\n",
    "\n",
    "    return Button(\n",
    "        f\"Count: {count}\",\n",
    "        on_click=increment,\n",
    "        spacing=1,\n",
    "        sx={\"minWidth\": 220, \"m\": 1},\n",
    "    )\n",
    "\n",
    "\n",
    "@component\n",
    "def LogWidget(handle=None):\n",
    "    messages, set_messages = use_state([])\n",
    "\n",
    "    use_imperative_handle(\n",
    "        handle,\n",
    "        lambda: {\n",
    "            \"add\": lambda text: set_messages(lambda prev: [*prev, text]),\n",
    "            \"clear\": lambda: set_messages([]),\n",
    "        },\n",
    "        [],\n",
    "    )\n",
    "\n",
    "    return Stack(\n",
    "        [\n",
    "            Typography(\"Logs:\", level=\"body-md\"),\n",
    "            *[Typography(f\"- {msg}\", level=\"body-sm\") for msg in messages],\n",
    "        ],\n",
    "        spacing=1,\n",
    "        sx={\"minWidth\": 220, \"m\": 1},\n",
    "    )"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "232ddf37",
   "metadata": {},
   "source": [
    "\n",
    "Let's render the counter widget in a cell:\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "49c545b8",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.pret+json": {
       "detach": false,
       "version_major": 0,
       "version_minor": 0,
       "view_data": {
        "chunk_idx": 0,
        "marshaler_id": "e3a218d86bc846ce823fafb1c1be7d80"
       }
      },
      "text/plain": [
       "<pret.render.Renderable object at 0x1103fa5f0>"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# remote refs to hold the counter actions\n",
    "counter_handle = use_ref()\n",
    "log_handle = use_ref()\n",
    "\n",
    "CounterWidget(\n",
    "    handle=counter_handle,\n",
    "    on_change=lambda v: log_handle.current.add(f\"Counter is now {v}\"),\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "82b6c128",
   "metadata": {},
   "source": [
    "Then the log widget in another cell:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "8f66fdf0",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.pret+json": {
       "detach": false,
       "version_major": 0,
       "version_minor": 0,
       "view_data": {
        "chunk_idx": 1,
        "marshaler_id": "e3a218d86bc846ce823fafb1c1be7d80"
       }
      },
      "text/plain": [
       "<pret.render.Renderable object at 0x1103faf80>"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "LogWidget(handle=log_handle)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "74e53bb2",
   "metadata": {},
   "source": [
    "\n",
    "Finally the reset button in another cell:\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "6072a8df",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.pret+json": {
       "detach": false,
       "version_major": 0,
       "version_minor": 0,
       "view_data": {
        "chunk_idx": 2,
        "marshaler_id": "e3a218d86bc846ce823fafb1c1be7d80"
       }
      },
      "text/plain": [
       "<pret.render.Renderable object at 0x1119bf970>"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Button(\n",
    "    \"Reset counter\",\n",
    "    on_click=lambda: counter_handle.current.reset(),\n",
    "    color=\"neutral\",\n",
    "    variant=\"outlined\",\n",
    "    sx={\"minWidth\": 220, \"m\": 1},\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "acccde5e",
   "metadata": {},
   "source": [
    "\n",
    "We could also have everything in the same cell using a `Stack` or `Grid` layout.\n",
    "\n",
    "You can observe that no UI state is shared between the two widgets: they talk and synchronize through events and commands.\n",
    "\n",
    "## Remote refs\n",
    "\n",
    "In the example above, we created two `use_ref()` references in the notebook to hold the handles of the two widgets. When these refs are created *in the notebook*, they are called *remote refs* because they are created on the server side and allow to control widgets running on the client side. `use_ref` is the **only** hook that can be used outside of a `@component` function, i.e., directly in the notebook.\n",
    "\n",
    "One advantage of this is that, should you refactor your app and move the widget creation from the notebook to a component, and the *remote* ref to a standard *local* ref, you just have to move everything in a `@component` function, and it should work like a charm.\n",
    "\n",
    "However, this comes with a few limitations :\n",
    "\n",
    "- you can only interact with fields on the handle that are functions (e.g., :white_check_mark: `handle.current.focus()`), not properties (e.g., :no_entry_sign: `handle.current.value`).\n",
    "- calling functions on remote refs is asynchronous, and the result is a future/promise. You cannot expect to get a return value immediately.\n",
    "\n",
    "## Widget factories\n",
    "\n",
    "As explained above, widgets are just components that follow some conventions. They should therefore not require access to the server/kernel state to be rendered. You may still need to configure a widget with some data from the server. This is where *widget factories* come in handy: a widget factory is a function that takes runs on the server and returns a Renderable widget that can be embedded in a Pret app.\n",
    "\n",
    "For instance, imagine a `DataFrame` widget that displays the content of a pandas DataFrame. DataFrame are not serializable, so the widget cannot directly use a dataframe during its rendering. Instead, we can create a widget factory that takes a DataFrame, prepares the data (e.g., serializes it to JSON) and returns a [Table][metanno.ui.Table] configured with this data.\n",
    "\n",
    "Observe how the factory function `DataFrameComponentFactory` is not decorated with `@component`: it is instead meant to run on the server and return a widget, which in turn can be rendered on the client.\n",
    "\n",
    "Here is a *component factory* that takes a pandas DataFrame and returns a Table Renderable element:\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "8518568b",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "from metanno import Table\n",
    "\n",
    "from pret.render import Renderable\n",
    "\n",
    "\n",
    "def DataFrameStaticViewFactory(df: pd.DataFrame, editable_columns=[]) -> Renderable:\n",
    "    # Prepare the data (e.g., serialize to JSON)\n",
    "    data = df.to_dict(orient=\"records\")\n",
    "    columns = [\n",
    "        {\n",
    "            \"name\": col,\n",
    "            \"key\": col,\n",
    "            \"filterable\": True,\n",
    "            \"kind\": \"text\"\n",
    "            if df.dtypes[col].kind in \"iufc\"\n",
    "            else \"boolean\"\n",
    "            if df.dtypes[col].kind == \"b\"\n",
    "            else \"text\",\n",
    "            \"editable\": col in editable_columns,\n",
    "        }\n",
    "        for col in df.columns\n",
    "    ]\n",
    "\n",
    "    # Return a Table component configured with the data\n",
    "    return Table(rows=data, columns=columns)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "42366fb2",
   "metadata": {},
   "source": [
    "\n",
    "Note that since [Table][metanno.ui.Table] expects the data to be prepared and updated for it by its caller (i.e., it is a controlled component), we have no way make this component dynamic.\n",
    "\n",
    "Now, here is a *widget factory* that takes a DataFrame and returns a configurable metanno [Table][metanno.ui.Table] widget. It will expose an imperative API to set/get filters and scroll to a given row and an event callback when a cell is changed:\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "2f8295df",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "\n",
    "from pret import server_only\n",
    "from pret.hooks import use_ref\n",
    "from pret.render import Renderable, component\n",
    "\n",
    "\n",
    "def DataFrameWidgetFactory(df: pd.DataFrame, handle=None, editable_columns=[]) -> Renderable:\n",
    "    # Prepare the data (e.g., serialize to JSON)\n",
    "    data = df.to_dict(orient=\"records\")\n",
    "    columns = [\n",
    "        {\n",
    "            \"name\": col,\n",
    "            \"key\": col,\n",
    "            \"filterable\": True,\n",
    "            \"kind\": \"text\"\n",
    "            if df.dtypes[col].kind in \"iufc\"\n",
    "            else \"boolean\"\n",
    "            if df.dtypes[col].kind == \"b\"\n",
    "            else \"text\",\n",
    "            \"editable\": col in editable_columns,\n",
    "        }\n",
    "        for col in df.columns\n",
    "    ]\n",
    "\n",
    "    @server_only\n",
    "    def handle_cell_change_server(row_id, row_idx, col_key, new_value):\n",
    "        df.at[row_idx, col_key] = new_value\n",
    "\n",
    "    @component\n",
    "    def Widget(handle=None, on_cell_change=None) -> Renderable:\n",
    "        # Internal state\n",
    "        filters, set_filters = use_state({})\n",
    "        table_handle = use_ref()\n",
    "        state_data, set_state_data = use_state(data)\n",
    "\n",
    "        use_imperative_handle(\n",
    "            handle,\n",
    "            lambda: {\n",
    "                \"set_filters\": set_filters,\n",
    "                \"get_filters\": lambda: filters,\n",
    "                \"scroll_to_row_idx\": lambda idx,\n",
    "                behavior=None: table_handle.current.scroll_to_row_idx(idx, behavior),\n",
    "            },\n",
    "            [],\n",
    "        )\n",
    "\n",
    "        @use_event_callback\n",
    "        def handle_filters_change(filters, col):\n",
    "            set_filters(filters)\n",
    "\n",
    "        @use_event_callback\n",
    "        def handle_cell_change(row_id, row_idx, col_key, new_value):\n",
    "            # Update local state to reflect the change\n",
    "            updated_data = list(state_data)\n",
    "            updated_data[row_idx] = {**updated_data[row_idx], col_key: new_value}\n",
    "            set_state_data(updated_data)\n",
    "            if on_cell_change is not None:\n",
    "                on_cell_change(row_id, row_idx, col_key, new_value)\n",
    "\n",
    "        # Return a Table component configured with the data and event handlers\n",
    "        return Table(\n",
    "            rows=state_data,\n",
    "            columns=columns,\n",
    "            filters=filters,\n",
    "            auto_filter=True,\n",
    "            on_filters_change=set_filters,\n",
    "            on_cell_change=handle_cell_change,\n",
    "            handle=table_handle,\n",
    "            style={\"height\": \"200px\"},\n",
    "        )\n",
    "\n",
    "    return Widget(handle=handle, on_cell_change=handle_cell_change_server)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5a691fb7",
   "metadata": {},
   "source": [
    "\n",
    "You can now use it by first creating a reference to hold the widget handle, then creating the widget using the factory, and finally rendering it:\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "ac4ee343",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.pret+json": {
       "detach": false,
       "version_major": 0,
       "version_minor": 0,
       "view_data": {
        "chunk_idx": 3,
        "marshaler_id": "e3a218d86bc846ce823fafb1c1be7d80"
       }
      },
      "text/plain": [
       "<pret.render.Renderable object at 0x1206f1c60>"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "df = pd.DataFrame([{\"a\": i, \"check\": False} for i in range(100)])\n",
    "handle = use_ref()\n",
    "DataFrameWidgetFactory(df, handle=handle)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9fc10e9a",
   "metadata": {},
   "source": [
    "\n",
    "Again : like widgets, a widget factory merely a code pattern: it is a function that runs on the server and returns a Renderable widget that can be rendered and controlled from the outside.\n",
    "Note how changing a cell in the table updates the underlying DataFrame on the server side.\n",
    "You can also control it imperatively by running the following code in another cell:\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "24611a79",
   "metadata": {
    "tags": [
     "no-exec"
    ]
   },
   "outputs": [],
   "source": [
    "# Scroll to row 50\n",
    "handle.current.scroll_to_row_idx(50);"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "69951476",
   "metadata": {},
   "source": [
    "\n",
    "or from a button:\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "f915258a",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.pret+json": {
       "detach": false,
       "version_major": 0,
       "version_minor": 0,
       "view_data": {
        "chunk_idx": 4,
        "marshaler_id": "e3a218d86bc846ce823fafb1c1be7d80"
       }
      },
      "text/plain": [
       "<pret.render.Renderable object at 0x1103f9bd0>"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from pret_joy import Button\n",
    "\n",
    "Button(\n",
    "    \"Go to row 50\",\n",
    "    on_click=lambda: handle.current.scroll_to_row_idx(50),\n",
    "    sx={\"m\": 1},\n",
    ")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.16"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
