From f531f56cc9bcc2c9d5a362a5802839301526bcea Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Thu, 18 Sep 2025 18:22:47 +0200 Subject: [PATCH] examples: Added a little example that shows how to use asynchronous http requests with Slint This is a little "stock ticker" that when manually refreshed uses async Rust and Python APIs to request new prices from a website and update a Slint model. In Rust, this demonstrates the use of `slint::spawn_local()` and in Python the use of our asyncio integration. --- Cargo.toml | 1 + examples/async-io/Cargo.toml | 21 ++++++++ examples/async-io/README.md | 19 ++++++++ examples/async-io/main.py | 45 +++++++++++++++++ examples/async-io/main.rs | 76 +++++++++++++++++++++++++++++ examples/async-io/pyproject.toml | 13 +++++ examples/async-io/stockticker.slint | 48 ++++++++++++++++++ 7 files changed, 223 insertions(+) create mode 100644 examples/async-io/Cargo.toml create mode 100644 examples/async-io/README.md create mode 100644 examples/async-io/main.py create mode 100644 examples/async-io/main.rs create mode 100644 examples/async-io/pyproject.toml create mode 100644 examples/async-io/stockticker.slint diff --git a/Cargo.toml b/Cargo.toml index 879caefa32c..68ede90c4ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ members = [ 'examples/mcu-board-support', 'examples/mcu-embassy', 'examples/uefi-demo', + 'examples/async-io', 'demos/weather-demo', 'demos/usecases/rust', 'helper_crates/const-field-offset', diff --git a/examples/async-io/Cargo.toml b/examples/async-io/Cargo.toml new file mode 100644 index 00000000000..6455756032e --- /dev/null +++ b/examples/async-io/Cargo.toml @@ -0,0 +1,21 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: MIT + +[package] +name = "stockticker" +version = "1.14.0" +authors = ["Slint Developers "] +edition = "2021" +publish = false +license = "MIT" + +[[bin]] +path = "main.rs" +name = "stockticker" + +[dependencies] +slint = { path = "../../api/rs/slint" } +async-compat = { version = "0.2.4" } +reqwest = { version = "0.12", features = ["json"] } +serde = "1.0" +serde_json = "1.0" diff --git a/examples/async-io/README.md b/examples/async-io/README.md new file mode 100644 index 00000000000..8e4ffec17cf --- /dev/null +++ b/examples/async-io/README.md @@ -0,0 +1,19 @@ + + +# Example demonstrating asynchronous API usage with Slint + +This example demonstrates how to use asynchronous I/O, by means of issuing HTTP GET requests, within the Slint event loop. + +The http GET requests fetch the closing prices of a few publicly traded stocks, and the result is show in the simple Slint UI. + +# Rust + +The Rust version is contained in [`main.rs`](./main.rs). It uses the `rewquest` crate to establish a network connection and issue the HTTP get requests, using Rusts `async` functions. These are run inside a future run with `slint::spawn_local()`, where we can await for the result of the network request and update the UI directly - as we're being run in the UI thread. + +Run the Rust version via `cargo run -p stockticker`. + +# Python + +The Python version is contained in [`main.py`](./main.py). It uses the `aiohttp` library to establish a network connection and issue the HTTP get requests, using Python's `asyncio` library. The entire request is started from within the `refresh` function that's marked to be `async` and connected to the `refresh` callback in `stockticker.slint`. Slint detects that the callback is async in Python and runs it as a new task. + +Run the Python version via `uv run main.py` in the `examples/async-io` directory. diff --git a/examples/async-io/main.py b/examples/async-io/main.py new file mode 100644 index 00000000000..831f8bf587c --- /dev/null +++ b/examples/async-io/main.py @@ -0,0 +1,45 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: MIT + +import slint +import asyncio +import aiohttp + +Symbol = slint.loader.stockticker.Symbol + + +async def refresh_stocks(model: slint.ListModel[Symbol]) -> None: + STOOQ_URL = "https://stooq.com/q/l/?s={symbols}&f=sd2t2ohlcvn&h&e=json" + url = STOOQ_URL.format(symbols="+".join([symbol.name for symbol in model])) + async with aiohttp.ClientSession() as session: + async with session.get(url) as resp: + json = await resp.json() + json_symbols = json["symbols"] + for row, symbol in enumerate(model): + data_for_symbol = next( + (sym for sym in json_symbols if sym["symbol"] == symbol.name), None + ) + if data_for_symbol: + symbol.price = data_for_symbol["close"] + model.set_row_data(row, symbol) + + +class MainWindow(slint.loader.stockticker.MainWindow): + def __init__(self): + super().__init__() + self.stocks = slint.ListModel( + [Symbol(name=name, price=0.0) for name in ["AAPL.US", "MSFT.US", "AMZN.US"]] + ) + + @slint.callback + async def refresh(self): + await refresh_stocks(self.stocks) + + +async def main() -> None: + main_window = MainWindow() + main_window.refresh() + main_window.show() + + +slint.run_event_loop(main()) diff --git a/examples/async-io/main.rs b/examples/async-io/main.rs new file mode 100644 index 00000000000..9419dbf7862 --- /dev/null +++ b/examples/async-io/main.rs @@ -0,0 +1,76 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: MIT + +use slint::Model; + +use async_compat::Compat; + +slint::slint! { + export { MainWindow, Symbol } from "stockticker.slint"; +} + +#[derive(serde::Deserialize, Debug, Clone)] +struct JsonSymbol { + symbol: String, + close: f32, +} + +#[derive(serde::Deserialize, Debug, Clone)] +struct JsonSymbols { + symbols: Vec, +} + +async fn refresh_stocks(model: slint::ModelRc) { + let url = format!( + "https://stooq.com/q/l/?s={}&f=sd2t2ohlcvn&h&e=json", + model.iter().map(|symbol| symbol.name.clone()).collect::>().join("+") + ); + + let response = match reqwest::get(url).await { + Ok(response) => response, + Err(err) => { + eprintln!("Error fetching update: {err}"); + return; + } + }; + + let json_symbols: JsonSymbols = match response.json().await { + Ok(json) => json, + Err(err) => { + eprintln!("Error decoding json response: {err}"); + return; + } + }; + + for row in 0..model.row_count() { + let mut symbol = model.row_data(row).unwrap(); + let Some(json_symbol) = json_symbols.symbols.iter().find(|s| *s.symbol == *symbol.name) + else { + continue; + }; + symbol.price = json_symbol.close; + model.set_row_data(row, symbol); + } +} + +fn main() -> Result<(), slint::PlatformError> { + let main_window = MainWindow::new()?; + + let model = slint::VecModel::from_slice(&[ + Symbol { name: "AAPL.US".into(), price: 0.0 }, + Symbol { name: "MSFT.US".into(), price: 0.0 }, + Symbol { name: "AMZN.US".into(), price: 0.0 }, + ]); + + main_window.set_stocks(model.clone().into()); + + main_window.show()?; + + slint::spawn_local(Compat::new(refresh_stocks(model.clone()))).unwrap(); + + main_window.on_refresh(move || { + slint::spawn_local(Compat::new(refresh_stocks(model.clone()))).unwrap(); + }); + + main_window.run() +} diff --git a/examples/async-io/pyproject.toml b/examples/async-io/pyproject.toml new file mode 100644 index 00000000000..fee28852557 --- /dev/null +++ b/examples/async-io/pyproject.toml @@ -0,0 +1,13 @@ +# Copyright © SixtyFPS GmbH +# SPDX-License-Identifier: MIT + +[project] +name = "python" +version = "1.14.0" +description = "Slint Stock Ticker Example for Python" +readme = "README.md" +requires-python = ">=3.12" +dependencies = ["aiohttp>=3.12.15", "slint"] + +[tool.uv.sources] +slint = { path = "../../api/python/slint", editable = true } diff --git a/examples/async-io/stockticker.slint b/examples/async-io/stockticker.slint new file mode 100644 index 00000000000..4c57173fc7c --- /dev/null +++ b/examples/async-io/stockticker.slint @@ -0,0 +1,48 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: MIT + +import { Button, VerticalBox, HorizontalBox } from "std-widgets.slint"; + +export struct Symbol { + name: string, + price: float, +} + +export component MainWindow inherits Window { + // The symbols and prices here are just to show something in the live-preview. They'll be replaced + // with fresh data on start-up. + in-out property <[Symbol]> stocks: [ + { + name: "AAPL.US", + price: 237.96, + }, + { + name: "MSFT.US", + price: 509.24, + }, + { name: "AMZN.US", price: 232.94 }, + ]; + callback refresh(); + + VerticalBox { + padding: 10px; + + for stock in root.stocks: HorizontalBox { + alignment: start; + Text { + text: stock.name; + } + + Text { + text: "$\{stock.price.to-fixed(2)}"; + } + } + + Button { + text: @tr("Refresh"); + clicked => { + root.refresh(); + } + } + } +}