Add event that's triggered when slider is released (#3353)

* Add code

* Add unit test

* CHANGELOG + notebook format

* Clean up

* Remove comment
This commit is contained in:
Freddy Boulton 2023-03-02 21:29:49 -05:00 committed by GitHub
parent 5c01a029ce
commit d8023d455f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 168 additions and 7 deletions

View File

@ -3,6 +3,17 @@
## New Features:
### Release event for Slider
Now you can trigger your python function to run when the slider is released as opposed to every slider change value!
Simply use the `release` method on the slider
```python
slider.release(function, inputs=[...], outputs=[...], api_name="predict")
```
By [@freddyaboulton](https://github.com/freddyaboulton) in [PR 3353](https://github.com/gradio-app/gradio/pull/3353)
### Dropdown Component Updates
The standard dropdown component now supports searching for choices. Also when `multiselect` is `True`, you can specify `max_choices` to set the maximum number of choices you want the user to be able to select from the dropdown component.

View File

@ -0,0 +1 @@
{"cells": [{"cell_type": "markdown", "id": 302934307671667531413257853548643485645, "metadata": {}, "source": ["# Gradio Demo: slider_release"]}, {"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", "\n", "def identity(x, state):\n", " state += 1\n", " return x, state, state\n", "\n", "\n", "with gr.Blocks() as demo:\n", " slider = gr.Slider(0, 100, step=0.1)\n", " state = gr.State(value=0)\n", " with gr.Row():\n", " number = gr.Number(label=\"On release\")\n", " number2 = gr.Number(label=\"Number of events fired\")\n", " slider.release(identity, inputs=[slider, state], outputs=[number, state, number2], api_name=\"predict\")\n", "\n", "if __name__ == \"__main__\":\n", " print(\"here\")\n", " demo.launch()\n", " print(demo.server_port)\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}

View File

@ -0,0 +1,20 @@
import gradio as gr
def identity(x, state):
state += 1
return x, state, state
with gr.Blocks() as demo:
slider = gr.Slider(0, 100, step=0.1)
state = gr.State(value=0)
with gr.Row():
number = gr.Number(label="On release")
number2 = gr.Number(label="Number of events fired")
slider.release(identity, inputs=[slider, state], outputs=[number, state, number2], api_name="predict")
if __name__ == "__main__":
print("here")
demo.launch()
print(demo.server_port)

View File

@ -40,6 +40,7 @@ from gradio.events import (
Clickable,
Editable,
Playable,
Releaseable,
Streamable,
Submittable,
Uploadable,
@ -616,7 +617,12 @@ class Number(
@document("change", "style")
class Slider(
FormComponent, Changeable, IOComponent, SimpleSerializable, NeighborInterpretable
FormComponent,
Changeable,
Releaseable,
IOComponent,
SimpleSerializable,
NeighborInterpretable,
):
"""
Creates a slider that ranges from `minimum` to `maximum` with a step size of `step`.
@ -624,7 +630,7 @@ class Slider(
Postprocessing: expects an {int} or {float} returned from function and sets slider value to it as long as it is within range.
Examples-format: A {float} or {int} representing the slider's value.
Demos: sentence_builder, generate_tone, titanic_survival, interface_random_slider, blocks_random_slider
Demos: sentence_builder, slider_release, generate_tone, titanic_survival, interface_random_slider, blocks_random_slider
Guides: create_your_own_friends_with_a_gan
"""

View File

@ -721,3 +721,60 @@ class Uploadable(EventListener):
every=every,
)
set_cancel_events(self, "upload", cancels)
class Releaseable(EventListener):
def release(
self,
fn: Callable | None,
inputs: Component | List[Component] | Set[Component] | None = None,
outputs: Component | List[Component] | None = None,
api_name: str | None = None,
scroll_to_output: bool = False,
show_progress: bool = True,
queue: bool | None = None,
batch: bool = False,
max_batch_size: int = 4,
preprocess: bool = True,
postprocess: bool = True,
cancels: Dict[str, Any] | List[Dict[str, Any]] | None = None,
every: float | None = None,
_js: str | None = None,
):
"""
This event is triggered when the user releases the mouse on this component (e.g. when the user releases the slider). This method can be used when this component is in a Gradio Blocks.
Parameters:
fn: Callable function
inputs: List of gradio.components to use as inputs. If the function takes no inputs, this should be an empty list.
outputs: List of gradio.components to use as outputs. If the function returns no outputs, this should be an empty list.
api_name: Defining this parameter exposes the endpoint in the api docs
scroll_to_output: If True, will scroll to output component on completion
show_progress: If True, will show progress animation while pending
queue: If True, will place the request on the queue, if the queue exists
batch: If True, then the function should process a batch of inputs, meaning that it should accept a list of input values for each parameter. The lists should be of equal length (and be up to length `max_batch_size`). The function is then *required* to return a tuple of lists (even if there is only 1 output component), with each list in the tuple corresponding to one output component.
max_batch_size: Maximum number of inputs to batch together if this is called from the queue (only relevant if batch=True)
preprocess: If False, will not run preprocessing of component data before running 'fn' (e.g. leaving it as a base64 string if this method is called with the `Image` component).
postprocess: If False, will not run postprocessing of component data before returning 'fn' output to the browser.
cancels: A list of other events to cancel when this event is triggered. For example, setting cancels=[click_event] will cancel the click_event, where click_event is the return value of another components .click method.
every: Run this event 'every' number of seconds while the client connection is open. Interpreted in seconds. Queue must be enabled.
"""
# _js: Optional frontend js method to run before running 'fn'. Input arguments for js method are values of 'inputs' and 'outputs', return should be a list of values for output components.
self.set_event_trigger(
"release",
fn,
inputs,
outputs,
preprocess=preprocess,
postprocess=postprocess,
scroll_to_output=scroll_to_output,
show_progress=show_progress,
api_name=api_name,
js=_js,
queue=queue,
batch=batch,
max_batch_size=max_batch_size,
every=every,
)
set_cancel_events(self, "release", cancels)

View File

@ -886,11 +886,11 @@ def tex2svg(formula, *args):
svg_start = xml_code.index("<svg ")
svg_code = xml_code[svg_start:]
svg_code = re.sub(r"<metadata>.*<\/metadata>", "", svg_code, flags=re.DOTALL)
svg_code = re.sub(r' width="[^"]+"', '', svg_code)
svg_code = re.sub(r' width="[^"]+"', "", svg_code)
height_match = re.search(r'height="([\d.]+)pt"', svg_code)
if height_match:
height = float(height_match.group(1))
new_height = height / FONTSIZE # conversion from pt to em
new_height = height / FONTSIZE # conversion from pt to em
svg_code = re.sub(r'height="[\d.]+pt"', f'height="{new_height}em"', svg_code)
copy_code = f"<span style='font-size: 0px'>{formula}</span>"
return f"{copy_code}{svg_code}"

View File

@ -37,5 +37,6 @@
{step}
disabled={mode === "static"}
on:change
on:release
/>
</Block>

View File

@ -0,0 +1,50 @@
import { test, expect, Page, Locator } from "@playwright/test";
//taken from: https://github.com/microsoft/playwright/issues/20032
async function changeSlider(
page: Page,
thumb: Locator,
slider: Locator,
targetPercentage: number
) {
const thumbBoundingBox = await thumb.boundingBox();
const sliderBoundingBox = await slider.boundingBox();
if (thumbBoundingBox === null || sliderBoundingBox === null) {
return; // NOTE it's probably better to throw an error here
}
// Start from the middle of the slider's thumb
const startPoint = {
x: thumbBoundingBox.x + thumbBoundingBox.width / 2,
y: thumbBoundingBox.y + thumbBoundingBox.height / 2
};
// Slide it to some endpoint determined by the target percentage
const endPoint = {
x: sliderBoundingBox.x + sliderBoundingBox.width * targetPercentage,
y: thumbBoundingBox.y + thumbBoundingBox.height / 2
};
await page.mouse.move(startPoint.x, startPoint.y);
await page.mouse.down();
await page.mouse.move(endPoint.x, endPoint.y);
await page.mouse.up();
}
test("slider release", async ({ page }) => {
await page.goto("http://127.0.0.1:7888/");
const slider = page.getByLabel("Slider");
const responsePromise = page.waitForResponse(
"http://127.0.0.1:7888/run/predict/"
);
await changeSlider(page, slider, slider, 0.7);
const response = await responsePromise;
const responseData = await response.json();
expect(responseData.data[0]).toBeGreaterThan(69.5);
expect(responseData.data[0]).toBeLessThan(71.0);
expect(responseData.data[2]).toEqual(1);
});

View File

@ -16,10 +16,16 @@
export let show_label: boolean;
const id = `range_id_${_id++}`;
const dispatch = createEventDispatcher<{ change: number }>();
const dispatch = createEventDispatcher<{ change: number; release: number }>();
function handle_release(e: MouseEvent) {
dispatch("release", value);
}
$: dispatch("change", value);
const clamp = () => (value = Math.min(Math.max(value, minimum), maximum));
const clamp = () => {
value = Math.min(Math.max(value, minimum), maximum);
};
</script>
<div class="wrap">
@ -48,6 +54,7 @@
max={maximum}
{step}
{disabled}
on:mouseup={handle_release}
/>
<style>

View File

@ -3,5 +3,13 @@ export default {
screenshot: "only-on-failure",
trace: "retain-on-failure"
},
globalSetup: "./playwright-setup.js"
globalSetup: "./playwright-setup.js",
workers: 1,
webServer: [
{
command: "GRADIO_SERVER_PORT=7888 python ../demo/slider_release/run.py",
port: 7888,
timeout: 120 * 1000
}
]
};