diff --git a/CHANGELOG.md b/CHANGELOG.md index c05932882f..978363d734 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ ## New Features: +- Trigger the release event when Slider number input is released or unfocused by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3589](https://github.com/gradio-app/gradio/pull/3589) +- Created Theme Builder, which allows users to create themes without writing any code, by [@aliabid94](https://github.com/aliabid94) in [PR 3664](https://github.com/gradio-app/gradio/pull/3664). Launch by: + + ```python + import gradio as gr + gr.themes.builder() + ``` + + ![Theme Builder](https://user-images.githubusercontent.com/7870876/228204929-d71cbba5-69c2-45b3-bd20-e3a201d98b12.png) + +- The `Dropdown` component now has a `allow_custom_value` parameter that lets users type in custom values not in the original list of choices. +- The `Colorpicker` component now has a `.blur()` event + ### Added a download button for videos! 📥 ![download_video](https://user-images.githubusercontent.com/41651716/227009612-9bc5fb72-2a44-4c55-9b7b-a0fa098e7f25.gif) diff --git a/demo/blocks_kitchen_sink/run.ipynb b/demo/blocks_kitchen_sink/run.ipynb index 952a02c28e..497d80c841 100644 --- a/demo/blocks_kitchen_sink/run.ipynb +++ b/demo/blocks_kitchen_sink/run.ipynb @@ -1 +1 @@ -{"cells": [{"cell_type": "markdown", "id": 302934307671667531413257853548643485645, "metadata": {}, "source": ["# Gradio Demo: blocks_kitchen_sink"]}, {"cell_type": "code", "execution_count": null, "id": 272996653310673477252411125948039410165, "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": 288918539441861185822528903084949547379, "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "import time\n", "from os.path import abspath, join, pardir\n", "\n", "KS_FILES = abspath(join(__file__, pardir, pardir, \"kitchen_sink\", \"files\"))\n", "\n", "base_theme = gr.themes.Base()\n", "default_theme = gr.themes.Default()\n", "monochrome_theme = gr.themes.Monochrome()\n", "soft_theme = gr.themes.Soft()\n", "glass_theme = gr.themes.Glass()\n", "\n", "with gr.Blocks(theme=base_theme) as demo:\n", " gr.Markdown(\n", " \"\"\"\n", " # Blocks Kitchen Sink\n", " This is a demo of most Gradio features. Test all themes and toogle dark mode (requires light mode setting)\n", " ## Elements\n", " - Use of Rows, Columns, Tabs, and Accordion\n", " - Use of Form elements: Textbox, Dropdown, Checkbox, Radio, Slider\n", " ## Other\n", " Other stuff\n", " - Buttons of variants: \"primary\", \"secondary\", \"stop\"\n", " - Embedded interface\n", " - Custom progress bar\n", " \"\"\"\n", " )\n", " toggle_dark = gr.Button(\"Toggle Dark\").style(full_width=False)\n", " toggle_dark.click(\n", " None,\n", " _js=\"\"\"\n", " () => { \n", " document.body.classList.toggle('dark');\n", " document.querySelector('gradio-app').style.backgroundColor = 'var(--background-primary)'\n", " }\n", " \"\"\",\n", " )\n", " theme_selector = gr.Radio(\n", " [\"Base\", \"Default\", \"Monochrome\", \"Soft\", \"Glass\"],\n", " value=\"Base\",\n", " label=\"Theme\",\n", " )\n", " theme_selector.change(\n", " None,\n", " theme_selector,\n", " None,\n", " _js=f\"\"\"\n", " (theme) => {{\n", " if (!document.querySelector('.theme-css')) {{\n", " var theme_elem = document.createElement('style');\n", " theme_elem.classList.add('theme-css');\n", " document.head.appendChild(theme_elem);\n", "\n", " var link_elem = document.createElement('link');\n", " link_elem.classList.add('link-css');\n", " link_elem.rel = 'stylesheet';\n", " document.head.appendChild(link_elem);\n", " }} else {{\n", " var theme_elem = document.querySelector('.theme-css');\n", " var link_elem = document.querySelector('.link-css');\n", " }}\n", " if (theme == \"Base\") {{\n", " var theme_css = `{base_theme._get_theme_css()}`;\n", " var link_css = `{base_theme._stylesheets[0]}`;\n", " }} else if (theme == \"Default\") {{\n", " var theme_css = `{default_theme._get_theme_css()}`;\n", " var link_css = `{default_theme._stylesheets[0]}`;\n", " }} else if (theme == \"Monochrome\") {{\n", " var theme_css = `{monochrome_theme._get_theme_css()}`;\n", " var link_css = `{monochrome_theme._stylesheets[0]}`;\n", " }} else if (theme == \"Soft\") {{\n", " var theme_css = `{soft_theme._get_theme_css()}`;\n", " var link_css = `{soft_theme._stylesheets[0]}`;\n", " }} else if (theme == \"Glass\") {{\n", " var theme_css = `{glass_theme._get_theme_css()}`;\n", " var link_css = `{glass_theme._stylesheets[0]}`;\n", " }}\n", " theme_elem.innerHTML = theme_css;\n", " link_elem.href = link_css;\n", " }}\n", " \"\"\",\n", " )\n", "\n", " name = gr.Textbox(\n", " label=\"Name (select)\",\n", " info=\"Full name, including middle name. No special characters.\",\n", " placeholder=\"John Doe\",\n", " value=\"John Doe\",\n", " interactive=True,\n", " )\n", "\n", " with gr.Row():\n", " slider1 = gr.Slider(label=\"Slider 1\")\n", " slider2 = gr.Slider(label=\"Slider 2\")\n", " checkboxes = gr.CheckboxGroup([\"A\", \"B\", \"C\"], label=\"Checkbox Group (select)\")\n", "\n", " with gr.Row():\n", " with gr.Column(variant=\"panel\", scale=1):\n", " gr.Markdown(\"## Panel 1\")\n", " radio = gr.Radio(\n", " [\"A\", \"B\", \"C\"],\n", " label=\"Radio (select)\",\n", " info=\"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\",\n", " )\n", " drop = gr.Dropdown([\"Option 1\", \"Option 2\", \"Option 3\"], show_label=False)\n", " drop_2 = gr.Dropdown(\n", " [\"Option A\", \"Option B\", \"Option C\"],\n", " multiselect=True,\n", " value=[\"Option A\"],\n", " label=\"Dropdown (select)\",\n", " interactive=True,\n", " )\n", " check = gr.Checkbox(label=\"Go\")\n", " with gr.Column(variant=\"panel\", scale=2):\n", " img = gr.Image(\n", " \"https://gradio.app/assets/img/header-image.jpg\", label=\"Image\"\n", " ).style(height=320)\n", " with gr.Row():\n", " go_btn = gr.Button(\"Go\", label=\"Primary Button\", variant=\"primary\")\n", " clear_btn = gr.Button(\n", " \"Clear\", label=\"Secondary Button\", variant=\"secondary\"\n", " )\n", "\n", " def go(*args):\n", " time.sleep(3)\n", " return \"https://gradio.app/assets/img/header-image.jpg\"\n", "\n", " go_btn.click(go, [radio, drop, drop_2, check, name], img, api_name=\"go\")\n", "\n", " def clear():\n", " time.sleep(0.2)\n", " return None\n", "\n", " clear_btn.click(clear, None, img)\n", "\n", " with gr.Row():\n", " btn1 = gr.Button(\"Button 1\").style(size=\"sm\")\n", " btn2 = gr.UploadButton().style(size=\"sm\")\n", " stop_btn = gr.Button(\"Stop\", label=\"Stop Button\", variant=\"stop\").style(\n", " size=\"sm\"\n", " )\n", "\n", " gr.Examples(\n", " examples=[join(KS_FILES, \"lion.jpg\"), join(KS_FILES, \"tower.jpg\")],\n", " inputs=img,\n", " )\n", "\n", " gr.Examples(\n", " examples=[\n", " [\"A\", \"Option 1\", [\"Option B\"], True, join(KS_FILES, \"lion.jpg\")],\n", " [\n", " \"B\",\n", " \"Option 2\",\n", " [\"Option B\", \"Option C\"],\n", " False,\n", " join(KS_FILES, \"tower.jpg\"),\n", " ],\n", " ],\n", " inputs=[radio, drop, drop_2, check, img],\n", " label=\"Examples (select)\",\n", " )\n", "\n", " gr.Markdown(\"## Media Files\")\n", "\n", " with gr.Tabs() as tabs:\n", " with gr.Tab(\"Audio\"):\n", " with gr.Row():\n", " gr.Audio()\n", " gr.Audio(source=\"microphone\")\n", " gr.Audio(join(KS_FILES, \"cantina.wav\"))\n", " with gr.Tab(\"Other\"):\n", " # gr.Image(source=\"webcam\")\n", " gr.HTML(\n", " \"
\"\n", " )\n", " with gr.Row():\n", " dataframe = gr.Dataframe(\n", " value=[[1, 2, 3], [4, 5, 6], [7, 8, 9]], label=\"Dataframe (select)\"\n", " )\n", " gr.JSON(\n", " value={\"a\": 1, \"b\": 2, \"c\": {\"test\": \"a\", \"test2\": [1, 2, 3]}}, label=\"JSON\"\n", " )\n", " label = gr.Label(\n", " value={\"cat\": 0.7, \"dog\": 0.2, \"fish\": 0.1}, label=\"Label (select)\"\n", " )\n", " file = gr.File(label=\"File (select)\")\n", " with gr.Row():\n", " gr.ColorPicker()\n", " gr.Video(join(KS_FILES, \"world.mp4\"))\n", " gallery = gr.Gallery(\n", " [\n", " (join(KS_FILES, \"lion.jpg\"), \"lion\"),\n", " (join(KS_FILES, \"logo.png\"), \"logo\"),\n", " (join(KS_FILES, \"tower.jpg\"), \"tower\"),\n", " ],\n", " label=\"Gallery (select)\",\n", " )\n", "\n", " with gr.Row():\n", " with gr.Column(scale=2):\n", " highlight = gr.HighlightedText(\n", " [[\"The\", \"art\"], [\"dog\", \"noun\"], [\"is\", None], [\"fat\", \"adj\"]],\n", " label=\"Highlighted Text (select)\",\n", " )\n", " chatbot = gr.Chatbot([[\"Hello\", \"Hi\"]], label=\"Chatbot (select)\")\n", " chat_btn = gr.Button(\"Add messages\")\n", "\n", " def chat(history):\n", " time.sleep(2)\n", " yield [[\"How are you?\", \"I am good.\"]]\n", " time\n", "\n", " chat_btn.click(\n", " lambda history: history\n", " + [[\"How are you?\", \"I am good.\"]]\n", " + (time.sleep(2) or []),\n", " chatbot,\n", " chatbot,\n", " )\n", " with gr.Column(scale=1):\n", " with gr.Accordion(\"Select Info\"):\n", " gr.Markdown(\n", " \"Click on any part of any component with '(select)' in the label and see the SelectData data here.\"\n", " )\n", " select_index = gr.Textbox(label=\"Index\")\n", " select_value = gr.Textbox(label=\"Value\")\n", " select_selected = gr.Textbox(label=\"Selected\")\n", "\n", " selectables = [\n", " name,\n", " checkboxes,\n", " radio,\n", " drop_2,\n", " dataframe,\n", " label,\n", " file,\n", " highlight,\n", " chatbot,\n", " gallery,\n", " tabs\n", " ]\n", "\n", " def select_data(evt: gr.SelectData):\n", " return [\n", " evt.index,\n", " evt.value,\n", " evt.selected,\n", " ]\n", "\n", " for selectable in selectables:\n", " selectable.select(\n", " select_data,\n", " None,\n", " [select_index, select_value, select_selected],\n", " )\n", "\n", " gr.Markdown(\"## Dataset Examples\")\n", "\n", " component_example_set = [\n", " (gr.Audio(render=False), join(KS_FILES, \"cantina.wav\")),\n", " (gr.Checkbox(render=False), True),\n", " (gr.CheckboxGroup(render=False), [\"A\", \"B\"]),\n", " (gr.ColorPicker(render=False), \"#FF0000\"),\n", " (gr.Dataframe(render=False), [[1, 2, 3], [4, 5, 6]]),\n", " (gr.Dropdown(render=False), \"A\"),\n", " (gr.File(render=False), join(KS_FILES, \"lion.jpg\")),\n", " (gr.HTML(render=False), \"
Test
\"),\n", " (gr.Image(render=False), join(KS_FILES, \"lion.jpg\")),\n", " (gr.Markdown(render=False), \"# Test\"),\n", " (gr.Number(render=False), 1),\n", " (gr.Radio(render=False), \"A\"),\n", " (gr.Slider(render=False), 1),\n", " (gr.Textbox(render=False), \"A\"),\n", " (gr.Video(render=False), join(KS_FILES, \"world.mp4\")),\n", " ]\n", " gr.Dataset(\n", " components=[c for c, _ in component_example_set],\n", " samples=[[e for _, e in component_example_set]],\n", " )\n", "\n", " with gr.Tabs():\n", " for c, e in component_example_set:\n", " with gr.Tab(c.__class__.__name__):\n", " gr.Dataset(components=[c], samples=[[e]] * 3)\n", "\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch(file_directories=[KS_FILES])\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file +{"cells": [{"cell_type": "markdown", "id": 302934307671667531413257853548643485645, "metadata": {}, "source": ["# Gradio Demo: blocks_kitchen_sink"]}, {"cell_type": "code", "execution_count": null, "id": 272996653310673477252411125948039410165, "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": 288918539441861185822528903084949547379, "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "import time\n", "from os.path import abspath, join, pardir\n", "\n", "KS_FILES = abspath(join(__file__, pardir, pardir, \"kitchen_sink\", \"files\"))\n", "\n", "base_theme = gr.themes.Base()\n", "default_theme = gr.themes.Default()\n", "monochrome_theme = gr.themes.Monochrome()\n", "soft_theme = gr.themes.Soft()\n", "glass_theme = gr.themes.Glass()\n", "\n", "with gr.Blocks(theme=base_theme) as demo:\n", " gr.Markdown(\n", " \"\"\"\n", " # Blocks Kitchen Sink\n", " This is a demo of most Gradio features. Test all themes and toogle dark mode (requires light mode setting)\n", " ## Elements\n", " - Use of Rows, Columns, Tabs, and Accordion\n", " - Use of Form elements: Textbox, Dropdown, Checkbox, Radio, Slider\n", " ## Other\n", " Other stuff\n", " - Buttons of variants: \"primary\", \"secondary\", \"stop\"\n", " - Embedded interface\n", " - Custom progress bar\n", " \"\"\"\n", " )\n", " toggle_dark = gr.Button(\"Toggle Dark\").style(full_width=False)\n", " toggle_dark.click(\n", " None,\n", " _js=\"\"\"\n", " () => { \n", " document.body.classList.toggle('dark');\n", " document.querySelector('gradio-app').style.backgroundColor = 'var(--background-primary)'\n", " }\n", " \"\"\",\n", " )\n", " theme_selector = gr.Radio(\n", " [\"Base\", \"Default\", \"Monochrome\", \"Soft\", \"Glass\"],\n", " value=\"Base\",\n", " label=\"Theme\",\n", " )\n", " theme_selector.change(\n", " None,\n", " theme_selector,\n", " None,\n", " _js=f\"\"\"\n", " (theme) => {{\n", " if (!document.querySelector('.theme-css')) {{\n", " var theme_elem = document.createElement('style');\n", " theme_elem.classList.add('theme-css');\n", " document.head.appendChild(theme_elem);\n", "\n", " var link_elem = document.createElement('link');\n", " link_elem.classList.add('link-css');\n", " link_elem.rel = 'stylesheet';\n", " document.head.appendChild(link_elem);\n", " }} else {{\n", " var theme_elem = document.querySelector('.theme-css');\n", " var link_elem = document.querySelector('.link-css');\n", " }}\n", " if (theme == \"Base\") {{\n", " var theme_css = `{base_theme._get_theme_css()}`;\n", " var link_css = `{base_theme._stylesheets[0]}`;\n", " }} else if (theme == \"Default\") {{\n", " var theme_css = `{default_theme._get_theme_css()}`;\n", " var link_css = `{default_theme._stylesheets[0]}`;\n", " }} else if (theme == \"Monochrome\") {{\n", " var theme_css = `{monochrome_theme._get_theme_css()}`;\n", " var link_css = `{monochrome_theme._stylesheets[0]}`;\n", " }} else if (theme == \"Soft\") {{\n", " var theme_css = `{soft_theme._get_theme_css()}`;\n", " var link_css = `{soft_theme._stylesheets[0]}`;\n", " }} else if (theme == \"Glass\") {{\n", " var theme_css = `{glass_theme._get_theme_css()}`;\n", " var link_css = `{glass_theme._stylesheets[0]}`;\n", " }}\n", " theme_elem.innerHTML = theme_css;\n", " link_elem.href = link_css;\n", " }}\n", " \"\"\",\n", " )\n", "\n", " name = gr.Textbox(\n", " label=\"Name (select)\",\n", " info=\"Full name, including middle name. No special characters.\",\n", " placeholder=\"John Doe\",\n", " value=\"John Doe\",\n", " interactive=True,\n", " )\n", "\n", " with gr.Row():\n", " slider1 = gr.Slider(label=\"Slider 1\")\n", " slider2 = gr.Slider(label=\"Slider 2\")\n", " checkboxes = gr.CheckboxGroup([\"A\", \"B\", \"C\"], label=\"Checkbox Group (select)\")\n", "\n", " with gr.Row():\n", " with gr.Column(variant=\"panel\", scale=1):\n", " gr.Markdown(\"## Panel 1\")\n", " radio = gr.Radio(\n", " [\"A\", \"B\", \"C\"],\n", " label=\"Radio (select)\",\n", " info=\"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\",\n", " )\n", " drop = gr.Dropdown([\"Option 1\", \"Option 2\", \"Option 3\"], show_label=False)\n", " drop_2 = gr.Dropdown(\n", " [\"Option A\", \"Option B\", \"Option C\"],\n", " multiselect=True,\n", " value=[\"Option A\"],\n", " label=\"Dropdown (select)\",\n", " interactive=True,\n", " )\n", " check = gr.Checkbox(label=\"Go\")\n", " with gr.Column(variant=\"panel\", scale=2):\n", " img = gr.Image(\n", " \"https://gradio.app/assets/img/header-image.jpg\", label=\"Image\"\n", " ).style(height=320)\n", " with gr.Row():\n", " go_btn = gr.Button(\"Go\", label=\"Primary Button\", variant=\"primary\")\n", " clear_btn = gr.Button(\n", " \"Clear\", label=\"Secondary Button\", variant=\"secondary\"\n", " )\n", "\n", " def go(*args):\n", " time.sleep(3)\n", " return \"https://i.ibb.co/6BgKdSj/groot.jpg\"\n", "\n", " go_btn.click(go, [radio, drop, drop_2, check, name], img, api_name=\"go\")\n", "\n", " def clear():\n", " time.sleep(0.2)\n", " return None\n", "\n", " clear_btn.click(clear, None, img)\n", "\n", " with gr.Row():\n", " btn1 = gr.Button(\"Button 1\").style(size=\"sm\")\n", " btn2 = gr.UploadButton().style(size=\"sm\")\n", " stop_btn = gr.Button(\"Stop\", label=\"Stop Button\", variant=\"stop\").style(\n", " size=\"sm\"\n", " )\n", "\n", " gr.Examples(\n", " examples=[join(KS_FILES, \"lion.jpg\"), join(KS_FILES, \"tower.jpg\")],\n", " inputs=img,\n", " )\n", "\n", " gr.Examples(\n", " examples=[\n", " [\"A\", \"Option 1\", [\"Option B\"], True, join(KS_FILES, \"lion.jpg\")],\n", " [\n", " \"B\",\n", " \"Option 2\",\n", " [\"Option B\", \"Option C\"],\n", " False,\n", " join(KS_FILES, \"tower.jpg\"),\n", " ],\n", " ],\n", " inputs=[radio, drop, drop_2, check, img],\n", " label=\"Examples (select)\",\n", " )\n", "\n", " gr.Markdown(\"## Media Files\")\n", "\n", " with gr.Tabs() as tabs:\n", " with gr.Tab(\"Audio\"):\n", " with gr.Row():\n", " gr.Audio()\n", " gr.Audio(source=\"microphone\")\n", " gr.Audio(join(KS_FILES, \"cantina.wav\"))\n", " with gr.Tab(\"Other\"):\n", " # gr.Image(source=\"webcam\")\n", " gr.HTML(\n", " \"
\"\n", " )\n", " with gr.Row():\n", " dataframe = gr.Dataframe(\n", " value=[[1, 2, 3], [4, 5, 6], [7, 8, 9]], label=\"Dataframe (select)\"\n", " )\n", " gr.JSON(\n", " value={\"a\": 1, \"b\": 2, \"c\": {\"test\": \"a\", \"test2\": [1, 2, 3]}}, label=\"JSON\"\n", " )\n", " label = gr.Label(\n", " value={\"cat\": 0.7, \"dog\": 0.2, \"fish\": 0.1}, label=\"Label (select)\"\n", " )\n", " file = gr.File(label=\"File (select)\")\n", " with gr.Row():\n", " gr.ColorPicker()\n", " gr.Video(join(KS_FILES, \"world.mp4\"))\n", " gallery = gr.Gallery(\n", " [\n", " (join(KS_FILES, \"lion.jpg\"), \"lion\"),\n", " (join(KS_FILES, \"logo.png\"), \"logo\"),\n", " (join(KS_FILES, \"tower.jpg\"), \"tower\"),\n", " ],\n", " label=\"Gallery (select)\",\n", " )\n", "\n", " with gr.Row():\n", " with gr.Column(scale=2):\n", " highlight = gr.HighlightedText(\n", " [[\"The\", \"art\"], [\"dog\", \"noun\"], [\"is\", None], [\"fat\", \"adj\"]],\n", " label=\"Highlighted Text (select)\",\n", " )\n", " chatbot = gr.Chatbot([[\"Hello\", \"Hi\"]], label=\"Chatbot (select)\")\n", " chat_btn = gr.Button(\"Add messages\")\n", "\n", " def chat(history):\n", " time.sleep(2)\n", " yield [[\"How are you?\", \"I am good.\"]]\n", " time\n", "\n", " chat_btn.click(\n", " lambda history: history\n", " + [[\"How are you?\", \"I am good.\"]]\n", " + (time.sleep(2) or []),\n", " chatbot,\n", " chatbot,\n", " )\n", " with gr.Column(scale=1):\n", " with gr.Accordion(\"Select Info\"):\n", " gr.Markdown(\n", " \"Click on any part of any component with '(select)' in the label and see the SelectData data here.\"\n", " )\n", " select_index = gr.Textbox(label=\"Index\")\n", " select_value = gr.Textbox(label=\"Value\")\n", " select_selected = gr.Textbox(label=\"Selected\")\n", "\n", " selectables = [\n", " name,\n", " checkboxes,\n", " radio,\n", " drop_2,\n", " dataframe,\n", " label,\n", " file,\n", " highlight,\n", " chatbot,\n", " gallery,\n", " tabs\n", " ]\n", "\n", " def select_data(evt: gr.SelectData):\n", " return [\n", " evt.index,\n", " evt.value,\n", " evt.selected,\n", " ]\n", "\n", " for selectable in selectables:\n", " selectable.select(\n", " select_data,\n", " None,\n", " [select_index, select_value, select_selected],\n", " )\n", "\n", " gr.Markdown(\"## Dataset Examples\")\n", "\n", " component_example_set = [\n", " (gr.Audio(render=False), join(KS_FILES, \"cantina.wav\")),\n", " (gr.Checkbox(render=False), True),\n", " (gr.CheckboxGroup(render=False), [\"A\", \"B\"]),\n", " (gr.ColorPicker(render=False), \"#FF0000\"),\n", " (gr.Dataframe(render=False), [[1, 2, 3], [4, 5, 6]]),\n", " (gr.Dropdown(render=False), \"A\"),\n", " (gr.File(render=False), join(KS_FILES, \"lion.jpg\")),\n", " (gr.HTML(render=False), \"
Test
\"),\n", " (gr.Image(render=False), join(KS_FILES, \"lion.jpg\")),\n", " (gr.Markdown(render=False), \"# Test\"),\n", " (gr.Number(render=False), 1),\n", " (gr.Radio(render=False), \"A\"),\n", " (gr.Slider(render=False), 1),\n", " (gr.Textbox(render=False), \"A\"),\n", " (gr.Video(render=False), join(KS_FILES, \"world.mp4\")),\n", " ]\n", " gr.Dataset(\n", " components=[c for c, _ in component_example_set],\n", " samples=[[e for _, e in component_example_set]],\n", " )\n", "\n", " with gr.Tabs():\n", " for c, e in component_example_set:\n", " with gr.Tab(c.__class__.__name__):\n", " gr.Dataset(components=[c], samples=[[e]] * 3)\n", "\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch(file_directories=[KS_FILES])\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file diff --git a/demo/blocks_kitchen_sink/run.py b/demo/blocks_kitchen_sink/run.py index dbd011bc08..5cdbd94783 100644 --- a/demo/blocks_kitchen_sink/run.py +++ b/demo/blocks_kitchen_sink/run.py @@ -123,7 +123,7 @@ with gr.Blocks(theme=base_theme) as demo: def go(*args): time.sleep(3) - return "https://gradio.app/assets/img/header-image.jpg" + return "https://i.ibb.co/6BgKdSj/groot.jpg" go_btn.click(go, [radio, drop, drop_2, check, name], img, api_name="go") diff --git a/demo/theme_builder/run.ipynb b/demo/theme_builder/run.ipynb new file mode 100644 index 0000000000..c8fc1f0e81 --- /dev/null +++ b/demo/theme_builder/run.ipynb @@ -0,0 +1 @@ +{"cells": [{"cell_type": "markdown", "id": 302934307671667531413257853548643485645, "metadata": {}, "source": ["# Gradio Demo: theme_builder"]}, {"cell_type": "code", "execution_count": null, "id": 272996653310673477252411125948039410165, "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": 288918539441861185822528903084949547379, "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "\n", "demo = gr.themes.builder\n", "\n", "if __name__ == \"__main__\":\n", " demo()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5} \ No newline at end of file diff --git a/demo/theme_builder/run.py b/demo/theme_builder/run.py new file mode 100644 index 0000000000..3d089dbf28 --- /dev/null +++ b/demo/theme_builder/run.py @@ -0,0 +1,6 @@ +import gradio as gr + +demo = gr.themes.builder + +if __name__ == "__main__": + demo() \ No newline at end of file diff --git a/gradio/components.py b/gradio/components.py index 5715884384..faaaa82e51 100644 --- a/gradio/components.py +++ b/gradio/components.py @@ -1383,7 +1383,9 @@ class Radio( @document("style") -class Dropdown(Changeable, Selectable, IOComponent, SimpleSerializable, FormComponent): +class Dropdown( + Changeable, Selectable, Blurrable, IOComponent, SimpleSerializable, FormComponent +): """ Creates a dropdown of choices from which entries can be selected. Preprocessing: passes the value of the selected dropdown entry as a {str} or its index as an {int} into the function, depending on `type`. @@ -1408,6 +1410,7 @@ class Dropdown(Changeable, Selectable, IOComponent, SimpleSerializable, FormComp visible: bool = True, elem_id: str | None = None, elem_classes: List[str] | str | None = None, + allow_custom_value: bool = False, **kwargs, ): """ @@ -1425,6 +1428,7 @@ class Dropdown(Changeable, Selectable, IOComponent, SimpleSerializable, FormComp visible: If False, component will be hidden. elem_id: An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles. elem_classes: An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles. + allow_custom_value: If True, allows user to enter a custom value that is not in the list of choices. """ self.choices = choices or [] valid_types = ["value", "index"] @@ -1442,6 +1446,11 @@ class Dropdown(Changeable, Selectable, IOComponent, SimpleSerializable, FormComp "The `max_choices` parameter is ignored when `multiselect` is False." ) self.max_choices = max_choices + self.allow_custom_value = allow_custom_value + if multiselect and allow_custom_value: + raise ValueError( + "Custom values are not supported when `multiselect` is True." + ) self.test_input = self.choices[0] if len(self.choices) else None self.interpret_by_tokens = False self.select: EventListenerMethod @@ -1484,6 +1493,7 @@ class Dropdown(Changeable, Selectable, IOComponent, SimpleSerializable, FormComp "value": self.value, "multiselect": self.multiselect, "max_choices": self.max_choices, + "allow_custom_value": self.allow_custom_value, **IOComponent.get_config(self), } @@ -1494,6 +1504,7 @@ class Dropdown(Changeable, Selectable, IOComponent, SimpleSerializable, FormComp label: str | None = None, show_label: bool | None = None, interactive: bool | None = None, + placeholder: str | None = None, visible: bool | None = None, ): return { @@ -1504,6 +1515,7 @@ class Dropdown(Changeable, Selectable, IOComponent, SimpleSerializable, FormComp "visible": visible, "value": value, "interactive": interactive, + "placeholder": placeholder, "__type__": "update", } @@ -1525,7 +1537,7 @@ class Dropdown(Changeable, Selectable, IOComponent, SimpleSerializable, FormComp return [self.choices.index(c) for c in x] else: if isinstance(x, str): - return self.choices.index(x) + return self.choices.index(x) if x in self.choices else None else: raise ValueError( "Unknown type: " @@ -3407,7 +3419,7 @@ class UploadButton(Clickable, Uploadable, IOComponent, FileSerializable): @document("style") -class ColorPicker(Changeable, Submittable, IOComponent, StringSerializable): +class ColorPicker(Changeable, Submittable, Blurrable, IOComponent, StringSerializable): """ Creates a color picker for user to select a color as string input. Preprocessing: passes selected color value as a {str} into the function. diff --git a/gradio/themes/__init__.py b/gradio/themes/__init__.py index 583b37b242..e80e52b6fe 100644 --- a/gradio/themes/__init__.py +++ b/gradio/themes/__init__.py @@ -8,3 +8,9 @@ from gradio.themes.utils import colors, sizes # noqa: F401 from gradio.themes.utils.colors import Color # noqa: F401 from gradio.themes.utils.fonts import Font, GoogleFont # noqa: F401 from gradio.themes.utils.sizes import Size # noqa: F401 + + +def builder(*args, **kwargs): + from gradio.themes.builder import demo + + demo.launch(*args, **kwargs) diff --git a/gradio/themes/base.py b/gradio/themes/base.py index 43f246932e..8ba8b38298 100644 --- a/gradio/themes/base.py +++ b/gradio/themes/base.py @@ -473,7 +473,7 @@ class Base(ThemeClass): def set( self, *, - # Body + # Body Attributes: These set set the values for the entire body of the app. body_background_fill=None, body_background_fill_dark=None, body_text_color=None, @@ -483,7 +483,7 @@ class Base(ThemeClass): body_text_color_subdued_dark=None, body_text_weight=None, embed_radius=None, - # Core Colors + # Element Colors: These set the colors for common elements. background_fill_primary=None, background_fill_primary_dark=None, background_fill_secondary=None, @@ -495,7 +495,7 @@ class Base(ThemeClass): color_accent=None, color_accent_soft=None, color_accent_soft_dark=None, - # Text + # Text: This sets the text styling for text elements. link_text_color=None, link_text_color_dark=None, link_text_color_active=None, @@ -507,13 +507,13 @@ class Base(ThemeClass): prose_text_size=None, prose_text_weight=None, prose_header_text_weight=None, - # Shadows + # Shadows: These set the high-level shadow rendering styles. These variables are often referenced by other component-specific shadow variables. shadow_drop=None, shadow_drop_lg=None, shadow_inset=None, shadow_spread=None, shadow_spread_dark=None, - # Layout Atoms + # Layout Atoms: These set the style for common layout elements, such as the blocks that wrap components. block_background_fill=None, block_background_fill_dark=None, block_border_color=None, @@ -565,7 +565,7 @@ class Base(ThemeClass): panel_border_width_dark=None, section_header_text_size=None, section_header_text_weight=None, - # Component Atoms + # Component Atoms: These set the style for elements within components. checkbox_background_color=None, checkbox_background_color_dark=None, checkbox_background_color_focus=None, @@ -656,25 +656,21 @@ class Base(ThemeClass): table_radius=None, table_row_focus=None, table_row_focus_dark=None, - # Buttons + # Buttons: These set the style for buttons. button_border_width=None, button_border_width_dark=None, - button_cancel_background_fill=None, - button_cancel_background_fill_dark=None, - button_cancel_background_fill_hover=None, - button_cancel_background_fill_hover_dark=None, - button_cancel_border_color=None, - button_cancel_border_color_dark=None, - button_cancel_border_color_hover=None, - button_cancel_border_color_hover_dark=None, - button_cancel_text_color=None, - button_cancel_text_color_dark=None, - button_cancel_text_color_hover=None, - button_cancel_text_color_hover_dark=None, + button_shadow=None, + button_shadow_active=None, + button_shadow_hover=None, + button_transition=None, button_large_padding=None, button_large_radius=None, button_large_text_size=None, button_large_text_weight=None, + button_small_padding=None, + button_small_radius=None, + button_small_text_size=None, + button_small_text_weight=None, button_primary_background_fill=None, button_primary_background_fill_dark=None, button_primary_background_fill_hover=None, @@ -699,14 +695,18 @@ class Base(ThemeClass): button_secondary_text_color_dark=None, button_secondary_text_color_hover=None, button_secondary_text_color_hover_dark=None, - button_shadow=None, - button_shadow_active=None, - button_shadow_hover=None, - button_small_padding=None, - button_small_radius=None, - button_small_text_size=None, - button_small_text_weight=None, - button_transition=None, + button_cancel_background_fill=None, + button_cancel_background_fill_dark=None, + button_cancel_background_fill_hover=None, + button_cancel_background_fill_hover_dark=None, + button_cancel_border_color=None, + button_cancel_border_color_dark=None, + button_cancel_border_color_hover=None, + button_cancel_border_color_hover_dark=None, + button_cancel_text_color=None, + button_cancel_text_color_dark=None, + button_cancel_text_color_hover=None, + button_cancel_text_color_hover_dark=None, ) -> Base: """ Parameters: @@ -1509,7 +1509,7 @@ class Base(ThemeClass): self.prose_header_text_weight = prose_header_text_weight or getattr( self, "prose_header_text_weight", "600" ) - self.slider_color = slider_color or getattr(self, "slider_color", "") + self.slider_color = slider_color or getattr(self, "slider_color", "auto") self.slider_color_dark = slider_color_dark or getattr( self, "slider_color_dark", None ) diff --git a/gradio/themes/builder.py b/gradio/themes/builder.py new file mode 100644 index 0000000000..e09f7fcf72 --- /dev/null +++ b/gradio/themes/builder.py @@ -0,0 +1,993 @@ +import inspect +import time +from typing import Iterable + +import gradio as gr + +themes = [ + gr.themes.Base, + gr.themes.Default, + gr.themes.Soft, + gr.themes.Monochrome, + gr.themes.Glass, +] +colors = gr.themes.Color.all +sizes = gr.themes.Size.all + +palette_range = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950] +size_range = ["xxs", "xs", "sm", "md", "lg", "xl", "xxl"] +docs_theme_core = gr.documentation.document_fn(gr.themes.Base.__init__, gr.themes.Base)[ + 1 +] +docs_theme_vars = gr.documentation.document_fn(gr.themes.Base.set, gr.themes.Base)[1] + + +def get_docstr(var): + for parameters in docs_theme_core + docs_theme_vars: + if parameters["name"] == var: + return parameters["doc"] + raise ValueError(f"Variable {var} not found in theme documentation.") + + +def get_doc_theme_var_groups(): + source = inspect.getsource(gr.themes.Base.set) + groups = [] + group, desc, variables, flat_variables = None, None, [], [] + for line in source.splitlines(): + line = line.strip() + if line.startswith(")"): + break + elif line.startswith("# "): + if group is not None: + groups.append((group, desc, variables)) + group, desc = line[2:].split(": ") + variables = [] + elif "=" in line: + var = line.split("=")[0] + variables.append(var) + flat_variables.append(var) + groups.append((group, desc, variables)) + return groups, flat_variables + + +variable_groups, flat_variables = get_doc_theme_var_groups() + +css = """ +.gradio-container { + overflow: visible !important; + max-width: none !important; +} +#controls { + max-height: 100vh; + flex-wrap: unset; + overflow-y: scroll; + position: sticky; + top: 0; +} +#controls::-webkit-scrollbar { + -webkit-appearance: none; + width: 7px; +} + +#controls::-webkit-scrollbar-thumb { + border-radius: 4px; + background-color: rgba(0, 0, 0, .5); + box-shadow: 0 0 1px rgba(255, 255, 255, .5); +} +""" + +with gr.Blocks(theme=gr.themes.Base(), css=css, title="Gradio Theme Builder") as demo: + with gr.Row(): + with gr.Column(scale=1, elem_id="controls", min_width=400): + with gr.Row(): + undo_btn = gr.Button("Undo").style(size="sm") + dark_mode_btn = gr.Button("Dark Mode", variant="primary").style( + size="sm" + ) + with gr.Tabs(): + with gr.TabItem("Source Theme"): + gr.Markdown( + """ + Select a base theme below you would like to build off of. Note: when you click 'Load Theme', all variable values in other tabs will be overwritten! + """ + ) + base_theme_dropdown = gr.Dropdown( + [theme.__name__ for theme in themes], + value="Base", + show_label=False, + ) + load_theme_btn = gr.Button("Load Theme", elem_id="load_theme") + with gr.TabItem("Core Colors"): + gr.Markdown( + """Set the three hues of the theme: `primary_hue`, `secondary_hue`, and `neutral_hue`. + Each of these is a palette ranging from 50 to 950 in brightness. Pick a preset palette - optionally, open the accordion to overwrite specific values. + Note that these variables do not affect elements directly, but are referenced by other variables with asterisks, such as `*primary_200` or `*neutral_950`.""" + ) + primary_hue = gr.Dropdown( + [color.name for color in colors], label="Primary Hue" + ) + with gr.Accordion(label="Primary Hue Palette", open=False): + primary_hues = [] + for i in palette_range: + primary_hues.append( + gr.ColorPicker( + label=f"primary_{i}", + ) + ) + + secondary_hue = gr.Dropdown( + [color.name for color in colors], label="Secondary Hue" + ) + with gr.Accordion(label="Secondary Hue Palette", open=False): + secondary_hues = [] + for i in palette_range: + secondary_hues.append( + gr.ColorPicker( + label=f"secondary_{i}", + ) + ) + + neutral_hue = gr.Dropdown( + [color.name for color in colors], label="Neutral hue" + ) + with gr.Accordion(label="Neutral Hue Palette", open=False): + neutral_hues = [] + for i in palette_range: + neutral_hues.append( + gr.ColorPicker( + label=f"neutral_{i}", + ) + ) + + with gr.TabItem("Core Sizing"): + gr.Markdown( + """Set the sizing of the theme via: `text_size`, `spacing_size`, and `radius_size`. + Each of these is set to a collection of sizes ranging from `xxs` to `xxl`. Pick a preset size collection - optionally, open the accordion to overwrite specific values. + Note that these variables do not affect elements directly, but are referenced by other variables with asterisks, such as `*spacing_xl` or `*text_sm`. + """ + ) + text_size = gr.Dropdown( + [size.name for size in sizes if size.name.startswith("text_")], + label="Text Size", + ) + with gr.Accordion(label="Text Size Range", open=False): + text_sizes = [] + for i in size_range: + text_sizes.append( + gr.Textbox( + label=f"text_{i}", + ) + ) + + spacing_size = gr.Dropdown( + [ + size.name + for size in sizes + if size.name.startswith("spacing_") + ], + label="Spacing Size", + ) + with gr.Accordion(label="Spacing Size Range", open=False): + spacing_sizes = [] + for i in size_range: + spacing_sizes.append( + gr.Textbox( + label=f"spacing_{i}", + ) + ) + + radius_size = gr.Dropdown( + [ + size.name + for size in sizes + if size.name.startswith("radius_") + ], + label="Radius Size", + ) + with gr.Accordion(label="Radius Size Range", open=False): + radius_sizes = [] + for i in size_range: + radius_sizes.append( + gr.Textbox( + label=f"radius_{i}", + ) + ) + + with gr.TabItem("Core Fonts"): + gr.Markdown( + """Set the main `font` and the monospace `font_mono` here. + Set up to 4 values for each (fallbacks in case a font is not available). + Check "Google Font" if font should be loaded from Google Fonts. + """ + ) + gr.Markdown("### Main Font") + main_fonts, main_is_google = [], [] + for i in range(4): + with gr.Row(): + font = gr.Textbox(label=f"Font {i + 1}") + font_is_google = gr.Checkbox(label="Google Font") + main_fonts.append(font) + main_is_google.append(font_is_google) + + mono_fonts, mono_is_google = [], [] + gr.Markdown("### Monospace Font") + for i in range(4): + with gr.Row(): + font = gr.Textbox(label=f"Font {i + 1}") + font_is_google = gr.Checkbox(label="Google Font") + mono_fonts.append(font) + mono_is_google.append(font_is_google) + + theme_var_input = [] + + core_color_suggestions = ( + [f"*primary_{i}" for i in palette_range] + + [f"*secondary_{i}" for i in palette_range] + + [f"*neutral_{i}" for i in palette_range] + ) + + variable_suggestions = { + "fill": core_color_suggestions[:], + "color": core_color_suggestions[:], + "text_size": [f"*text_{i}" for i in size_range], + "radius": [f"*radius_{i}" for i in size_range], + "padding": [f"*spacing_{i}" for i in size_range], + "gap": [f"*spacing_{i}" for i in size_range], + "weight": [ + "100", + "200", + "300", + "400", + "500", + "600", + "700", + "800", + ], + "shadow": ["none"], + "border_width": [], + } + for variable in flat_variables: + if variable.endswith("_dark"): + continue + for style_type in variable_suggestions: + if style_type in variable: + variable_suggestions[style_type].append("*" + variable) + break + + variable_suggestions["fill"], variable_suggestions["color"] = ( + variable_suggestions["fill"] + + variable_suggestions["color"][len(core_color_suggestions) :], + variable_suggestions["color"] + + variable_suggestions["fill"][len(core_color_suggestions) :], + ) + + for group, desc, variables in variable_groups: + with gr.TabItem(group): + gr.Markdown( + desc + + "\nYou can set these to one of the dropdown values, or clear the dropdown to set a custom value." + ) + for variable in variables: + suggestions = [] + for style_type in variable_suggestions: + if style_type in variable: + suggestions = variable_suggestions[style_type][:] + if "*" + variable in suggestions: + suggestions.remove("*" + variable) + break + dropdown = gr.Dropdown( + label=variable, + info=get_docstr(variable), + choices=suggestions, + allow_custom_value=True, + ) + theme_var_input.append(dropdown) + + # App + + with gr.Column(scale=6, elem_id="app"): + with gr.Column(variant="panel"): + gr.Markdown( + """ + # Theme Builder + Welcome to the theme builder. The left panel is where you create the theme. The different aspects of the theme are broken down into different tabs. Here's how to navigate them: + 1. First, set the "Source Theme". This will set the default values that you can override. + 2. Set the "Core Colors", "Core Sizing" and "Core Fonts". These are the core variables that are used to build the rest of the theme. + 3. The rest of the tabs set specific CSS theme variables. These control finer aspects of the UI. Within these theme variables, you can reference the core variables and other theme variables using the variable name preceded by an asterisk, e.g. `*primary_50` or `*body_text_color`. Clear the dropdown to set a custom value. + 4. Once you have finished your theme, click on "View Code" below to see how you can integrate the theme into your app. You can also click on "Upload to Hub" to upload your theme to the Hugging Face Hub, where others can download and use your theme. + """ + ) + with gr.Accordion("View Code", open=False): + output_code = gr.Code(language="python") + with gr.Accordion("Upload to Hub", open=False): + gr.Markdown( + "You can save your theme on the Hugging Face Hub. HF API write token can be found [here](https://huggingface.co/settings/tokens)." + ) + with gr.Row(): + theme_name = gr.Textbox(label="Theme Name") + theme_hf_token = gr.Textbox(label="Hugging Face Write Token") + theme_version = gr.Textbox( + label="Version", + placeholder="Leave blank to automatically update version.", + ) + upload_to_hub_btn = gr.Button("Upload to Hub") + theme_upload_status = gr.Markdown(visible=False) + + gr.Markdown("Below this panel is a dummy app to demo your theme.") + + name = gr.Textbox( + label="Name", + info="Full name, including middle name. No special characters.", + placeholder="John Doe", + value="John Doe", + interactive=True, + ) + + with gr.Row(): + slider1 = gr.Slider(label="Slider 1") + slider2 = gr.Slider(label="Slider 2") + gr.CheckboxGroup(["A", "B", "C"], label="Checkbox Group") + + with gr.Row(): + with gr.Column(variant="panel", scale=1): + gr.Markdown("## Panel 1") + radio = gr.Radio( + ["A", "B", "C"], + label="Radio", + info="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", + ) + drop = gr.Dropdown( + ["Option 1", "Option 2", "Option 3"], show_label=False + ) + drop_2 = gr.Dropdown( + ["Option A", "Option B", "Option C"], + multiselect=True, + value=["Option A"], + label="Dropdown", + interactive=True, + ) + check = gr.Checkbox(label="Go") + with gr.Column(variant="panel", scale=2): + img = gr.Image( + "https://i.ibb.co/6BgKdSj/groot.jpg", label="Image" + ).style(height=320) + with gr.Row(): + go_btn = gr.Button( + "Go", label="Primary Button", variant="primary" + ) + clear_btn = gr.Button( + "Clear", label="Secondary Button", variant="secondary" + ) + + def go(*args): + time.sleep(3) + return "https://i.ibb.co/6BgKdSj/groot.jpg" + + go_btn.click( + go, [radio, drop, drop_2, check, name], img, api_name="go" + ) + + def clear(): + time.sleep(0.2) + return None + + clear_btn.click(clear, None, img) + + with gr.Row(): + btn1 = gr.Button("Button 1").style(size="sm") + btn2 = gr.UploadButton().style(size="sm") + stop_btn = gr.Button( + "Stop", label="Stop Button", variant="stop" + ).style(size="sm") + + gr.Examples( + examples=[ + [ + "A", + "Option 1", + ["Option B"], + True, + ], + [ + "B", + "Option 2", + ["Option B", "Option C"], + False, + ], + ], + inputs=[radio, drop, drop_2, check], + label="Examples", + ) + + with gr.Row(): + gr.Dataframe(value=[[1, 2, 3], [4, 5, 6], [7, 8, 9]], label="Dataframe") + gr.JSON( + value={"a": 1, "b": 2, "c": {"test": "a", "test2": [1, 2, 3]}}, + label="JSON", + ) + gr.Label(value={"cat": 0.7, "dog": 0.2, "fish": 0.1}) + gr.File() + with gr.Row(): + gr.ColorPicker() + gr.Video( + "https://gradio-static-files.s3.us-west-2.amazonaws.com/world.mp4" + ) + gr.Gallery( + [ + ( + "https://gradio-static-files.s3.us-west-2.amazonaws.com/lion.jpg", + "lion", + ), + ( + "https://gradio-static-files.s3.us-west-2.amazonaws.com/logo.png", + "logo", + ), + ( + "https://gradio-static-files.s3.us-west-2.amazonaws.com/tower.jpg", + "tower", + ), + ] + ).style(height="200px", grid=2) + + with gr.Row(): + with gr.Column(scale=2): + chatbot = gr.Chatbot([("Hello", "Hi")], label="Chatbot") + chat_btn = gr.Button("Add messages") + + def chat(history): + time.sleep(2) + yield [["How are you?", "I am good."]] + + chat_btn.click( + lambda history: history + + [["How are you?", "I am good."]] + + (time.sleep(2) or []), + chatbot, + chatbot, + ) + with gr.Column(scale=1): + with gr.Accordion("Advanced Settings"): + gr.Markdown("Hello") + gr.Number(label="Chatbot control 1") + gr.Number(label="Chatbot control 2") + gr.Number(label="Chatbot control 3") + + # Event Listeners + + secret_css = gr.Textbox(visible=False) + secret_font = gr.JSON(visible=False) + + demo.load( # doing this via python was not working for some reason, so using this hacky method for now + None, + None, + None, + _js="""() => { + document.head.innerHTML += ""; + let evt_listener = window.setTimeout( + () => { + load_theme_btn = document.querySelector('#load_theme'); + if (load_theme_btn) { + load_theme_btn.click(); + window.clearTimeout(evt_listener); + } + }, + 100 + ); + }""", + ) + + theme_inputs = ( + [primary_hue, secondary_hue, neutral_hue] + + primary_hues + + secondary_hues + + neutral_hues + + [text_size, spacing_size, radius_size] + + text_sizes + + spacing_sizes + + radius_sizes + + main_fonts + + main_is_google + + mono_fonts + + mono_is_google + + theme_var_input + ) + + def load_theme(theme_name): + theme = [theme for theme in themes if theme.__name__ == theme_name][0] + + expand_color = lambda color: list( + [ + color.c50, + color.c100, + color.c200, + color.c300, + color.c400, + color.c500, + color.c600, + color.c700, + color.c800, + color.c900, + color.c950, + ] + ) + expand_size = lambda size: list( + [ + size.xxs, + size.xs, + size.sm, + size.md, + size.lg, + size.xl, + size.xxl, + ] + ) + parameters = inspect.signature(theme.__init__).parameters + primary_hue = parameters["primary_hue"].default + secondary_hue = parameters["secondary_hue"].default + neutral_hue = parameters["neutral_hue"].default + text_size = parameters["text_size"].default + spacing_size = parameters["spacing_size"].default + radius_size = parameters["radius_size"].default + + theme = theme() + + font = theme._font[:4] + font_mono = theme._font_mono[:4] + font_is_google = [isinstance(f, gr.themes.GoogleFont) for f in font] + font_mono_is_google = [ + isinstance(f, gr.themes.GoogleFont) for f in font_mono + ] + font = [f.name for f in font] + font_mono = [f.name for f in font_mono] + pad_to_4 = lambda x: x + [None] * (4 - len(x)) + + font, font_is_google = pad_to_4(font), pad_to_4(font_is_google) + font_mono, font_mono_is_google = pad_to_4(font_mono), pad_to_4( + font_mono_is_google + ) + + var_output = [] + for variable in flat_variables: + theme_val = getattr(theme, variable) + if theme_val is None and variable.endswith("_dark"): + theme_val = getattr(theme, variable[:-5]) + var_output.append(theme_val) + + return ( + [primary_hue.name, secondary_hue.name, neutral_hue.name] + + expand_color(primary_hue) + + expand_color(secondary_hue) + + expand_color(neutral_hue) + + [text_size.name, spacing_size.name, radius_size.name] + + expand_size(text_size) + + expand_size(spacing_size) + + expand_size(radius_size) + + font + + font_is_google + + font_mono + + font_mono_is_google + + var_output + ) + + def generate_theme_code( + base_theme, final_theme, core_variables, final_main_fonts, final_mono_fonts + ): + base_theme_name = base_theme + base_theme = [theme for theme in themes if theme.__name__ == base_theme][ + 0 + ]() + + parameters = inspect.signature(base_theme.__init__).parameters + primary_hue = parameters["primary_hue"].default + secondary_hue = parameters["secondary_hue"].default + neutral_hue = parameters["neutral_hue"].default + text_size = parameters["text_size"].default + spacing_size = parameters["spacing_size"].default + radius_size = parameters["radius_size"].default + font = parameters["font"].default + font = [font] if not isinstance(font, Iterable) else font + font = [ + gr.themes.Font(f) if not isinstance(f, gr.themes.Font) else f + for f in font + ] + font_mono = parameters["font_mono"].default + font_mono = ( + [font_mono] if not isinstance(font_mono, Iterable) else font_mono + ) + font_mono = [ + gr.themes.Font(f) if not isinstance(f, gr.themes.Font) else f + for f in font_mono + ] + + core_diffs = {} + specific_core_diffs = {} + core_var_names = [ + "primary_hue", + "secondary_hue", + "neutral_hue", + "text_size", + "spacing_size", + "radius_size", + ] + for value_name, base_value, source_class, final_value in zip( + core_var_names, + [ + primary_hue, + secondary_hue, + neutral_hue, + text_size, + spacing_size, + radius_size, + ], + [ + gr.themes.Color, + gr.themes.Color, + gr.themes.Color, + gr.themes.Size, + gr.themes.Size, + gr.themes.Size, + ], + core_variables, + ): + if base_value.name != final_value: + core_diffs[value_name] = final_value + source_obj = [ + obj for obj in source_class.all if obj.name == final_value + ][0] + final_attr_values = {} + diff = False + for attr in dir(source_obj): + if attr in ["all", "name"] or attr.startswith("_"): + continue + final_theme_attr = ( + value_name.split("_")[0] + + "_" + + (attr[1:] if source_class == gr.themes.Color else attr) + ) + final_attr_values[final_theme_attr] = getattr( + final_theme, final_theme_attr + ) + if getattr(source_obj, attr) != final_attr_values[final_theme_attr]: + diff = True + if diff: + specific_core_diffs[value_name] = (source_class, final_attr_values) + + font_diffs = {} + + final_main_fonts = [font for font in final_main_fonts if font[0]] + final_mono_fonts = [font for font in final_mono_fonts if font[0]] + font = font[:4] + font_mono = font_mono[:4] + for base_font_set, theme_font_set, font_set_name in [ + (font, final_main_fonts, "font"), + (font_mono, final_mono_fonts, "font_mono"), + ]: + if len(base_font_set) != len(theme_font_set) or any( + base_font.name != theme_font[0] + or isinstance(base_font, gr.themes.GoogleFont) != theme_font[1] + for base_font, theme_font in zip(base_font_set, theme_font_set) + ): + font_diffs[font_set_name] = [ + f"gr.themes.GoogleFont('{font_name}')" + if is_google_font + else f"'{font_name}'" + for font_name, is_google_font in theme_font_set + ] + + newline = "\n" + + core_diffs_code = "" + if len(core_diffs) + len(specific_core_diffs) > 0: + for var_name in core_var_names: + if var_name in specific_core_diffs: + cls, vals = specific_core_diffs[var_name] + core_diffs_code += f""" {var_name}=gr.themes.{cls.__name__}({', '.join(f'''{k}="{v}"''' for k, v in vals.items())}),\n""" + elif var_name in core_diffs: + core_diffs_code += ( + f""" {var_name}="{core_diffs[var_name]}",\n""" + ) + + font_diffs_code = "" + + if len(font_diffs) > 0: + font_diffs_code = "".join( + [ + f""" {font_set_name}=[{", ".join(fonts)}],\n""" + for font_set_name, fonts in font_diffs.items() + ] + ) + var_diffs = {} + for variable in flat_variables: + base_theme_val = getattr(base_theme, variable) + final_theme_val = getattr(final_theme, variable) + if base_theme_val is None and variable.endswith("_dark"): + base_theme_val = getattr(base_theme, variable[:-5]) + if base_theme_val != final_theme_val: + var_diffs[variable] = getattr(final_theme, variable) + + newline = "\n" + + vars_diff_code = "" + if len(var_diffs) > 0: + vars_diff_code = f""".set( + {(',' + newline + " ").join([f"{k}='{v}'" for k, v in var_diffs.items()])} +)""" + + output = f""" +import gradio as gr + +theme = gr.themes.{base_theme_name}({newline if core_diffs_code or font_diffs_code else ""}{core_diffs_code}{font_diffs_code}){vars_diff_code} + +with gr.Blocks(theme=theme) as demo: + ...""" + return output + + history = gr.State([]) + current_theme = gr.State(None) + + def render_variables(history, base_theme, *args): + primary_hue, secondary_hue, neutral_hue = args[0:3] + primary_hues = args[3 : 3 + len(palette_range)] + secondary_hues = args[3 + len(palette_range) : 3 + 2 * len(palette_range)] + neutral_hues = args[3 + 2 * len(palette_range) : 3 + 3 * len(palette_range)] + text_size, spacing_size, radius_size = args[ + 3 + 3 * len(palette_range) : 6 + 3 * len(palette_range) + ] + text_sizes = args[ + 6 + + 3 * len(palette_range) : 6 + + 3 * len(palette_range) + + len(size_range) + ] + spacing_sizes = args[ + 6 + + 3 * len(palette_range) + + len(size_range) : 6 + + 3 * len(palette_range) + + 2 * len(size_range) + ] + radius_sizes = args[ + 6 + + 3 * len(palette_range) + + 2 * len(size_range) : 6 + + 3 * len(palette_range) + + 3 * len(size_range) + ] + main_fonts = args[ + 6 + + 3 * len(palette_range) + + 3 * len(size_range) : 6 + + 3 * len(palette_range) + + 3 * len(size_range) + + 4 + ] + main_is_google = args[ + 6 + + 3 * len(palette_range) + + 3 * len(size_range) + + 4 : 6 + + 3 * len(palette_range) + + 3 * len(size_range) + + 8 + ] + mono_fonts = args[ + 6 + + 3 * len(palette_range) + + 3 * len(size_range) + + 8 : 6 + + 3 * len(palette_range) + + 3 * len(size_range) + + 12 + ] + mono_is_google = args[ + 6 + + 3 * len(palette_range) + + 3 * len(size_range) + + 12 : 6 + + 3 * len(palette_range) + + 3 * len(size_range) + + 16 + ] + remaining_args = args[ + 6 + 3 * len(palette_range) + 3 * len(size_range) + 16 : + ] + + final_primary_color = gr.themes.Color(*primary_hues) + final_secondary_color = gr.themes.Color(*secondary_hues) + final_neutral_color = gr.themes.Color(*neutral_hues) + final_text_size = gr.themes.Size(*text_sizes) + final_spacing_size = gr.themes.Size(*spacing_sizes) + final_radius_size = gr.themes.Size(*radius_sizes) + + final_main_fonts = [] + font_weights = set() + for attr, val in zip(flat_variables, remaining_args): + if "weight" in attr: + font_weights.add(val) + font_weights = sorted(font_weights) + + for main_font, is_google in zip(main_fonts, main_is_google): + if not main_font: + continue + if is_google: + main_font = gr.themes.GoogleFont(main_font, weights=font_weights) + final_main_fonts.append(main_font) + final_mono_fonts = [] + for mono_font, is_google in zip(mono_fonts, mono_is_google): + if not mono_font: + continue + if is_google: + mono_font = gr.themes.GoogleFont(mono_font, weights=font_weights) + final_mono_fonts.append(mono_font) + + theme = gr.themes.Base( + primary_hue=final_primary_color, + secondary_hue=final_secondary_color, + neutral_hue=final_neutral_color, + text_size=final_text_size, + spacing_size=final_spacing_size, + radius_size=final_radius_size, + font=final_main_fonts, + font_mono=final_mono_fonts, + ) + + theme.set( + **{attr: val for attr, val in zip(flat_variables, remaining_args)} + ) + new_step = (base_theme, args) + if len(history) == 0 or str(history[-1]) != str(new_step): + history.append(new_step) + + return ( + history, + theme._get_theme_css(), + theme._stylesheets, + generate_theme_code( + base_theme, + theme, + ( + primary_hue, + secondary_hue, + neutral_hue, + text_size, + spacing_size, + radius_size, + ), + list(zip(main_fonts, main_is_google)), + list(zip(mono_fonts, mono_is_google)), + ), + theme, + ) + + def attach_rerender(evt_listener): + return evt_listener( + render_variables, + [history, base_theme_dropdown] + theme_inputs, + [history, secret_css, secret_font, output_code, current_theme], + ).then( + None, + [secret_css, secret_font], + None, + _js="""(css, fonts) => { + document.getElementById('theme_css').innerHTML = css; + let existing_font_links = document.querySelectorAll('link[rel="stylesheet"][href^="https://fonts.googleapis.com/css"]'); + existing_font_links.forEach(link => { + if (fonts.includes(link.href)) { + fonts = fonts.filter(font => font != link.href); + } else { + link.remove(); + } + }); + fonts.forEach(font => { + let link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = font; + document.head.appendChild(link); + }); + }""", + ) + + def load_color(color_name): + color = [color for color in colors if color.name == color_name][0] + return [getattr(color, f"c{i}") for i in palette_range] + + attach_rerender(primary_hue.select(load_color, primary_hue, primary_hues).then) + attach_rerender( + secondary_hue.select(load_color, secondary_hue, secondary_hues).then + ) + attach_rerender(neutral_hue.select(load_color, neutral_hue, neutral_hues).then) + for hue_set in (primary_hues, secondary_hues, neutral_hues): + for hue in hue_set: + attach_rerender(hue.blur) + + def load_size(size_name): + size = [size for size in sizes if size.name == size_name][0] + return [getattr(size, i) for i in size_range] + + attach_rerender(text_size.change(load_size, text_size, text_sizes).then) + attach_rerender( + spacing_size.change(load_size, spacing_size, spacing_sizes).then + ) + attach_rerender(radius_size.change(load_size, radius_size, radius_sizes).then) + + attach_rerender( + load_theme_btn.click(load_theme, base_theme_dropdown, theme_inputs).then + ) + + for theme_box in ( + text_sizes + spacing_sizes + radius_sizes + main_fonts + mono_fonts + ): + attach_rerender(theme_box.blur) + attach_rerender(theme_box.submit) + for theme_box in theme_var_input: + attach_rerender(theme_box.blur) + attach_rerender(theme_box.select) + for checkbox in main_is_google + mono_is_google: + attach_rerender(checkbox.select) + + dark_mode_btn.click( + None, + None, + None, + _js="""() => { + if (document.querySelectorAll('.dark').length) { + document.querySelectorAll('.dark').forEach(el => el.classList.remove('dark')); + } else { + document.querySelector('body').classList.add('dark'); + } + }""", + ) + + def undo(history_var): + if len(history_var) <= 1: + return {history: gr.skip()} + else: + history_var.pop() + old = history_var.pop() + return [history_var, old[0]] + list(old[1]) + + attach_rerender( + undo_btn.click( + undo, [history], [history, base_theme_dropdown] + theme_inputs + ).then + ) + + def upload_to_hub(data): + try: + theme_url = data[current_theme].push_to_hub( + repo_name=data[theme_name], + version=data[theme_version] or None, + hf_token=data[theme_hf_token], + theme_name=data[theme_name], + ) + space_name = "/".join(theme_url.split("/")[-2:]) + return ( + gr.Markdown.update( + value=f"Theme uploaded [here!]({theme_url})! Load it as `gr.Blocks(theme='{space_name}')`", + visible=True, + ), + "Upload to Hub", + ) + except Exception as e: + return ( + gr.Markdown.update( + value=f"Error: {e}", + visible=True, + ), + "Upload to Hub", + ) + + upload_to_hub_btn.click(lambda: "Uploading...", None, upload_to_hub_btn).then( + upload_to_hub, + { + current_theme, + theme_name, + theme_hf_token, + theme_version, + }, + [theme_upload_status, upload_to_hub_btn], + ) + + +if __name__ == "__main__": + demo.launch() diff --git a/gradio/themes/utils/fonts.py b/gradio/themes/utils/fonts.py index 97f30ab158..d51dbbfdf4 100644 --- a/gradio/themes/utils/fonts.py +++ b/gradio/themes/utils/fonts.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +from typing import Iterable class FontEncoder(json.JSONEncoder): @@ -41,5 +42,9 @@ class Font: class GoogleFont(Font): + def __init__(self, name: str, weights: Iterable[int] = (400, 600)): + self.name = name + self.weights = weights + def stylesheet(self) -> str: - return f'https://fonts.googleapis.com/css2?family={self.name.replace(" ", "+")}:wght@400;600&display=swap' + return f'https://fonts.googleapis.com/css2?family={self.name.replace(" ", "+")}:wght@{";".join(str(weight) for weight in self.weights)}&display=swap' diff --git a/guides/06_other-tutorials/theming-guide.md b/guides/06_other-tutorials/theming-guide.md index 7636cf86e1..c327c86800 100644 --- a/guides/06_other-tutorials/theming-guide.md +++ b/guides/06_other-tutorials/theming-guide.md @@ -27,6 +27,24 @@ Gradio comes with a set of prebuilt themes which you can load from `gr.themes.*` Each of these themes set values for hundreds of CSS variables. You can use prebuilt themes as a starting point for your own custom themes, or you can create your own themes from scratch. Let's take a look at each approach. +## Using the Theme Builder + +The easiest way to build a theme is using the Theme Builder. To launch the Theme Builder locally, run the following code: + +```python +import gradio as gr + +gr.themes.builder() +``` + +$demo_theme_builder + +You can use the Theme Builder running on Spaces above, though it runs much faster when you launch it locally via `gr.themes.builder()`. + +As you edit the values in the Theme Builder, the app will preview updates in real time. You can download the code to generate the theme you've created so you can use it in any Gradio app. + +In the rest of the guide, we will cover building themes programatically. + ## Extending Themes via the Constructor Although each theme has hundreds of CSS variables, the values for most these variables are drawn from 8 core variables which can be set through the constructor of each prebuilt theme. Modifying these 8 arguments allows you to quickly change the look and feel of your app. diff --git a/js/app/src/components/ColorPicker/ColorPicker.svelte b/js/app/src/components/ColorPicker/ColorPicker.svelte index 4b87e6679c..ed9d85d576 100644 --- a/js/app/src/components/ColorPicker/ColorPicker.svelte +++ b/js/app/src/components/ColorPicker/ColorPicker.svelte @@ -37,6 +37,7 @@ {show_label} on:change on:submit + on:blur disabled={mode === "static"} /> diff --git a/js/app/src/components/Dropdown/Dropdown.svelte b/js/app/src/components/Dropdown/Dropdown.svelte index 89a322afa1..3f94c2d3eb 100644 --- a/js/app/src/components/Dropdown/Dropdown.svelte +++ b/js/app/src/components/Dropdown/Dropdown.svelte @@ -17,6 +17,7 @@ export let show_label: boolean; export let style: Styles = {}; export let loading_status: LoadingStatus; + export let allow_custom_value: boolean = false; export let mode: "static" | "dynamic"; @@ -43,8 +44,10 @@ {label} {info} {show_label} + {allow_custom_value} on:change on:select + on:blur disabled={mode === "static"} /> diff --git a/js/code/Index.svelte b/js/code/Index.svelte index f3ea31625e..86564e5cf4 100644 --- a/js/code/Index.svelte +++ b/js/code/Index.svelte @@ -35,7 +35,7 @@ - + {#if !value} @@ -51,7 +51,7 @@ - + diff --git a/js/code/interactive/Code.svelte b/js/code/interactive/Code.svelte index d1b6f40a0a..07b82d09a2 100644 --- a/js/code/interactive/Code.svelte +++ b/js/code/interactive/Code.svelte @@ -169,51 +169,10 @@ view = createEditorView(); return () => view?.destroy(); }); - - let padding = ""; - - const label_margin_var = "--block-label-margin"; - const label_padding_var = "--block-label-padding"; - const label_font_var = "--block-label-text-size"; - const label_line_height = "--line-sm"; - - const RE = /(?:([0-9\.]+[a-zA-Z%]+)\s*)/g; - function get_padding_for_label() { - const m = getComputedStyle(document.documentElement) - .getPropertyValue(label_margin_var) - .trim(); - const p = getComputedStyle(document.documentElement).getPropertyValue( - label_padding_var - ); - - const f = getComputedStyle(document.documentElement) - .getPropertyValue(label_font_var) - .trim(); - const l = getComputedStyle(document.documentElement) - .getPropertyValue(label_line_height) - .trim(); - - const x = p.match(RE); - if (!x) return; - - let [top, , bottom] = x.map((s) => s.trim()); - if (!bottom) bottom = top; - - const _margin = !m || m == "0" ? "" : ` ${m} +`; - const _height = /[a-zA-Z%]/.test(l) ? l : `(${f} * ${l})`; - - padding = `padding-top: calc(${top} + ${bottom} +${_margin} ${_height});`; - } - - get_padding_for_label();
-
+