mirror of
https://github.com/slint-ui/slint.git
synced 2025-09-30 13:51:13 +00:00
Added TodoMVC example (Rust mock version) (#5396)
* Added TodoMVC example (Rust mock version) * TodoMVC: use visible-width instead of width for selection items and format * TodoMVC: layout fix for qt checkbox * TdodoMVC: fix license issues in the example * Update examples/todo_mvc/ui/views/task_list_view.slint Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * TdodoMVC: fix license issues in the example * TodoMVC: code review changes * TodoMVC: code review changes * Update .reuse/dep5 Co-authored-by: Simon Hausmann <simon.hausmann@slint.dev> * Update examples/todo_mvc/rust/src/adapters/navigation_adapter.rs Co-authored-by: Simon Hausmann <simon.hausmann@slint.dev> * Update examples/todo_mvc/rust/src/adapters/navigation_adapter.rs Co-authored-by: Simon Hausmann <simon.hausmann@slint.dev> * TodoMVC: refactor task list model (code review feedback) * TodoMVC: code review feedback * Update examples/todo-mvc/rust/src/mvc/controllers/task_list_controller.rs Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * TodoMVC: add missing link in dep5 * dep5 fix --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Simon Hausmann <simon.hausmann@slint.dev>
This commit is contained in:
parent
a2e10f8c78
commit
0870585c32
41 changed files with 1651 additions and 1 deletions
5
.github/workflows/wasm_demos.yaml
vendored
5
.github/workflows/wasm_demos.yaml
vendored
|
@ -48,6 +48,11 @@ jobs:
|
|||
sed -i "s/#wasm# //" Cargo.toml
|
||||
wasm-pack build --release --target web
|
||||
working-directory: examples/todo/rust
|
||||
- name: Todo mvc demo WASM build
|
||||
run: |
|
||||
sed -i "s/#wasm# //" Cargo.toml
|
||||
wasm-pack build --release --target web
|
||||
working-directory: examples/todo-mvc/rust
|
||||
- name: Carousel demo WASM build
|
||||
run: |
|
||||
sed -i "s/#wasm# //" Cargo.toml
|
||||
|
|
|
@ -104,6 +104,10 @@ Files: internal/compiler/widgets/qt/_*.svg
|
|||
Copyright: Material Icons <https://github.com/material-icons/material-icons/blob/master/LICENSE>
|
||||
License: Apache-2.0
|
||||
|
||||
Files: examples/todo-mvc/assets/*.svg
|
||||
Copyright: Material Icons <https://github.com/material-icons/material-icons/blob/master/LICENSE>
|
||||
License: Apache-2.0
|
||||
|
||||
Files: internal/compiler/widgets/cosmic-base/_*.svg
|
||||
Copyright: "Cosmic Icons" by System76 <https://github.com/pop-os/cosmic-icons>
|
||||
License: CC-BY-SA-4.0
|
||||
|
|
|
@ -26,6 +26,7 @@ members = [
|
|||
'examples/printerdemo_mcu',
|
||||
'examples/slide_puzzle',
|
||||
'examples/todo/rust',
|
||||
'examples/todo-mvc/rust',
|
||||
'examples/virtual_keyboard/rust',
|
||||
'examples/carousel/rust',
|
||||
'examples/energy-monitor',
|
||||
|
|
|
@ -36,7 +36,7 @@ A fictional user interface of a device that monitors energy consumption in a bui
|
|||
|
||||
### [`todo`](./todo)
|
||||
|
||||
A simple todo mvc application
|
||||
A simple todo application
|
||||
|
||||
| `.slint` Design | Rust Source | C++ Source | NodeJS | Online wasm Preview | Open in SlintPad |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
|
@ -44,6 +44,14 @@ A simple todo mvc application
|
|||
|
||||

|
||||
|
||||
### [`todo-mvc`](./todo-mvc)
|
||||
|
||||
A simple todo application based on the [Model View Controller](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) pattern.
|
||||
|
||||
| `.slint` Design | Rust Source | Online wasm Preview | Open in SlintPad |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| [`todo.slint`](./todo/ui/todo.slint) | [`main.rs`](./todo/rust/main.rs) | [Online simulation](https://slint.dev/snapshots/master/demos/todo-mvc/) | [Preview in Online Code Editor](https://slint.dev/snapshots/master/editor?load_url=https://raw.githubusercontent.com/slint-ui/slint/master/examples/todo-mvc/ui/index.slint) |
|
||||
|
||||
### [`carousel`](./carousel)
|
||||
|
||||
A custom carousel widget that can be controlled by touch, mouse and keyboard
|
||||
|
|
3
examples/todo-mvc/assets/add.svg
Normal file
3
examples/todo-mvc/assets/add.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed">
|
||||
<path d="M440-440H200v-80h240v-240h80v240h240v80H520v240h-80v-240Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 187 B |
3
examples/todo-mvc/assets/close.svg
Normal file
3
examples/todo-mvc/assets/close.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed">
|
||||
<path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 227 B |
3
examples/todo-mvc/assets/remove.svg
Normal file
3
examples/todo-mvc/assets/remove.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368">
|
||||
<path d="M280-120q-33 0-56.5-23.5T200-200v-520h-40v-80h200v-40h240v40h200v80h-40v520q0 33-23.5 56.5T680-120H280Zm400-600H280v520h400v-520ZM360-280h80v-360h-80v360Zm160 0h80v-360h-80v360ZM280-720v520-520Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 324 B |
36
examples/todo-mvc/rust/Cargo.toml
Normal file
36
examples/todo-mvc/rust/Cargo.toml
Normal file
|
@ -0,0 +1,36 @@
|
|||
# Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
[package]
|
||||
name = "todo-mvc"
|
||||
version = "1.7.0"
|
||||
authors = ["Slint Developers <info@slint.dev>"]
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
publish = false
|
||||
license = "MIT"
|
||||
|
||||
[lib]
|
||||
crate-type = ["lib", "cdylib"]
|
||||
path = "src/lib.rs"
|
||||
name = "todo_lib_mvc"
|
||||
|
||||
[[bin]]
|
||||
path = "src/main.rs"
|
||||
name = "todo-mvc"
|
||||
|
||||
[dependencies]
|
||||
slint = { path = "../../../api/rs/slint", features = ["serde", "backend-android-activity-06"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
chrono = { version = "0.4" }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
wasm-bindgen = { version = "0.2" }
|
||||
console_error_panic_hook = "0.1.5"
|
||||
|
||||
[build-dependencies]
|
||||
slint-build = { path = "../../../api/rs/build" }
|
||||
|
||||
[dev-dependencies]
|
||||
i-slint-backend-testing = { workspace = true }
|
6
examples/todo-mvc/rust/build.rs
Normal file
6
examples/todo-mvc/rust/build.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
fn main() {
|
||||
slint_build::compile("../ui/index.slint").unwrap();
|
||||
}
|
41
examples/todo-mvc/rust/index.html
Normal file
41
examples/todo-mvc/rust/index.html
Normal file
|
@ -0,0 +1,41 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<!-- Copyright © SixtyFPS GmbH <info@slint.dev> -->
|
||||
<!-- SPDX-License-Identifier: MIT -->
|
||||
|
||||
<html>
|
||||
<!--
|
||||
This is a static html file used to display the wasm build.
|
||||
In order to generate the build
|
||||
- Run `wasm-pack build --release --target web` in this directory.
|
||||
-->
|
||||
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Slint Todo MVC Demo (Web Assembly version)</title>
|
||||
<link rel="stylesheet" href="https://slint.dev/css/demos-v1.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>This is the <a href="https://slint.dev">Slint</a> Todo Demo compiled to WebAssembly.</p>
|
||||
<div id="spinner" style="position: relative;">
|
||||
<div class="spinner">Loading...</div>
|
||||
</div>
|
||||
<canvas id="canvas" unselectable="on" data-slint-auto-resize-to-preferred="true"></canvas>
|
||||
<p class="links">
|
||||
<a href="https://github.com/slint-ui/slint/blob/master/examples/todo-mvc/">
|
||||
View Source Code on GitHub</a> -
|
||||
<a href="https://slint.dev/editor?load_demo=examples/todo-mvc/ui/index.slint">
|
||||
Open in SlintPad
|
||||
</a>
|
||||
</p>
|
||||
<script type="module">
|
||||
import init from './pkg/todo_lib_mvc.js';
|
||||
init().finally(() => {
|
||||
document.getElementById("spinner").remove();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
46
examples/todo-mvc/rust/src/callback.rs
Normal file
46
examples/todo-mvc/rust/src/callback.rs
Normal file
|
@ -0,0 +1,46 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::cell::Cell;
|
||||
|
||||
type CallbackWrapper<Arguments, Result = ()> =
|
||||
Cell<Option<Box<dyn FnMut(&Arguments, &mut Result)>>>;
|
||||
|
||||
pub struct Callback<Arguments: ?Sized, Result = ()> {
|
||||
callback: CallbackWrapper<Arguments, Result>,
|
||||
}
|
||||
|
||||
impl<Arguments: ?Sized, Res> Default for Callback<Arguments, Res> {
|
||||
fn default() -> Self {
|
||||
Self { callback: Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<Arguments: ?Sized, Result: Default> Callback<Arguments, Result> {
|
||||
pub fn on(&self, mut f: impl FnMut(&Arguments) -> Result + 'static) {
|
||||
self.callback.set(Some(Box::new(move |a: &Arguments, r: &mut Result| *r = f(a))));
|
||||
}
|
||||
|
||||
pub fn invoke(&self, a: &Arguments) -> Result {
|
||||
let mut result = Result::default();
|
||||
|
||||
if let Some(mut callback) = self.callback.take() {
|
||||
callback(a, &mut result);
|
||||
self.callback.set(Some(callback));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_invoke() {
|
||||
let callback: Callback<(i32, i32), i32> = Callback::default();
|
||||
callback.on(|(a, b)| a + b);
|
||||
assert_eq!(callback.invoke(&(3, 2)), 5);
|
||||
}
|
||||
}
|
39
examples/todo-mvc/rust/src/lib.rs
Normal file
39
examples/todo-mvc/rust/src/lib.rs
Normal file
|
@ -0,0 +1,39 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
pub mod mvc;
|
||||
pub mod ui;
|
||||
|
||||
mod callback;
|
||||
pub use callback::*;
|
||||
pub use slint::*;
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(start))]
|
||||
pub fn main() {
|
||||
let main_window = init();
|
||||
|
||||
main_window.run().unwrap();
|
||||
}
|
||||
|
||||
fn init() -> ui::MainWindow {
|
||||
let view_handle = ui::MainWindow::new().unwrap();
|
||||
|
||||
let task_list_controller = mvc::TaskListController::new(mvc::task_repo());
|
||||
ui::task_list_adapter::connect(&view_handle, task_list_controller.clone());
|
||||
ui::navigation_adapter::connect_task_list_controller(
|
||||
&view_handle,
|
||||
task_list_controller.clone(),
|
||||
);
|
||||
|
||||
let create_task_controller = mvc::CreateTaskController::new(mvc::date_time_repo());
|
||||
ui::create_task_adapter::connect(&view_handle, create_task_controller.clone());
|
||||
ui::navigation_adapter::connect_create_task_controller(&view_handle, create_task_controller);
|
||||
ui::create_task_adapter::connect_task_list_controller(&view_handle, task_list_controller);
|
||||
|
||||
view_handle
|
||||
}
|
||||
|
||||
// FIXME: android example
|
6
examples/todo-mvc/rust/src/main.rs
Normal file
6
examples/todo-mvc/rust/src/main.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
fn main() {
|
||||
todo_lib_mvc::main();
|
||||
}
|
11
examples/todo-mvc/rust/src/mvc.rs
Normal file
11
examples/todo-mvc/rust/src/mvc.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
mod controllers;
|
||||
pub use controllers::*;
|
||||
|
||||
mod models;
|
||||
pub use models::*;
|
||||
|
||||
mod repositories;
|
||||
pub use repositories::*;
|
8
examples/todo-mvc/rust/src/mvc/controllers.rs
Normal file
8
examples/todo-mvc/rust/src/mvc/controllers.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
mod create_task_controller;
|
||||
pub use create_task_controller::*;
|
||||
|
||||
mod task_list_controller;
|
||||
pub use task_list_controller::*;
|
|
@ -0,0 +1,120 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::mvc::{traits::DateTimeRepository, DateModel, TimeModel};
|
||||
use crate::{mvc, Callback};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CreateTaskController {
|
||||
repo: Rc<dyn mvc::traits::DateTimeRepository>,
|
||||
back_callback: Rc<Callback<(), ()>>,
|
||||
}
|
||||
|
||||
impl CreateTaskController {
|
||||
pub fn new(repo: impl DateTimeRepository + 'static) -> Self {
|
||||
Self { repo: Rc::new(repo), back_callback: Rc::new(Callback::default()) }
|
||||
}
|
||||
|
||||
pub fn current_date(&self) -> DateModel {
|
||||
self.repo.current_date()
|
||||
}
|
||||
|
||||
pub fn current_time(&self) -> TimeModel {
|
||||
self.repo.current_time()
|
||||
}
|
||||
|
||||
pub fn date_string(&self, date_model: DateModel) -> String {
|
||||
self.repo.date_to_string(date_model)
|
||||
}
|
||||
|
||||
pub fn time_string(&self, time_model: TimeModel) -> String {
|
||||
self.repo.time_to_string(time_model)
|
||||
}
|
||||
|
||||
pub fn back(&self) {
|
||||
self.back_callback.invoke(&());
|
||||
}
|
||||
|
||||
pub fn on_back(&self, mut callback: impl FnMut() + 'static) {
|
||||
self.back_callback.on(move |()| {
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn time_stamp(&self, date_model: DateModel, time_model: TimeModel) -> i32 {
|
||||
self.repo.time_stamp(date_model, time_model)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mvc::MockDateTimeRepository;
|
||||
use std::cell::Cell;
|
||||
|
||||
fn test_controller() -> CreateTaskController {
|
||||
CreateTaskController::new(MockDateTimeRepository::new(
|
||||
DateModel { year: 2024, month: 6, day: 12 },
|
||||
TimeModel { hour: 13, minute: 30, second: 29 },
|
||||
15,
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_current_date() {
|
||||
let controller = test_controller();
|
||||
assert_eq!(controller.current_date(), DateModel { year: 2024, month: 6, day: 12 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_current_time() {
|
||||
let controller = test_controller();
|
||||
assert_eq!(controller.current_time(), TimeModel { hour: 13, minute: 30, second: 29 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_date_string() {
|
||||
let controller = test_controller();
|
||||
assert_eq!(
|
||||
controller.date_string(DateModel { year: 2020, month: 10, day: 5 }).as_str(),
|
||||
"2020/10/5"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_string() {
|
||||
let controller = test_controller();
|
||||
assert_eq!(
|
||||
controller.time_string(TimeModel { hour: 10, minute: 12, second: 55 }).as_str(),
|
||||
"10:12"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_back() {
|
||||
let controller = test_controller();
|
||||
|
||||
let callback_invoked = Rc::new(Cell::new(false));
|
||||
|
||||
controller.on_back({
|
||||
let callback_invoked = callback_invoked.clone();
|
||||
|
||||
move || {
|
||||
callback_invoked.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
controller.back();
|
||||
|
||||
assert!(callback_invoked.get());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_stamp() {
|
||||
let controller = test_controller();
|
||||
|
||||
assert_eq!(controller.time_stamp(DateModel::default(), TimeModel::default()), 15);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
use slint::Model;
|
||||
use slint::ModelNotify;
|
||||
use slint::ModelRc;
|
||||
use slint::ModelTracker;
|
||||
|
||||
use crate::mvc;
|
||||
use crate::Callback;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TaskListController {
|
||||
task_model: TaskModel,
|
||||
show_create_task_callback: Rc<Callback<(), ()>>,
|
||||
}
|
||||
|
||||
impl TaskListController {
|
||||
pub fn new(repo: impl mvc::traits::TaskRepository + 'static) -> Self {
|
||||
Self {
|
||||
task_model: TaskModel::new(repo),
|
||||
show_create_task_callback: Rc::new(Callback::default()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn task_model(&self) -> ModelRc<mvc::TaskModel> {
|
||||
ModelRc::new(self.task_model.clone())
|
||||
}
|
||||
|
||||
pub fn toggle_done(&self, index: usize) {
|
||||
self.task_model.toggle_done(index)
|
||||
}
|
||||
|
||||
pub fn remove_task(&self, index: usize) {
|
||||
self.task_model.remove_task(index)
|
||||
}
|
||||
|
||||
pub fn create_task(&self, title: &str, due_date: i64) {
|
||||
self.task_model.push_task(mvc::TaskModel {
|
||||
title: title.into(),
|
||||
due_date,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn show_create_task(&self) {
|
||||
self.show_create_task_callback.invoke(&());
|
||||
}
|
||||
|
||||
pub fn on_show_create_task(&self, mut callback: impl FnMut() + 'static) {
|
||||
self.show_create_task_callback.on(move |()| {
|
||||
callback();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TaskModel {
|
||||
repo: Rc<dyn mvc::traits::TaskRepository>,
|
||||
notify: Rc<ModelNotify>,
|
||||
}
|
||||
|
||||
impl TaskModel {
|
||||
fn new(repo: impl mvc::traits::TaskRepository + 'static) -> Self {
|
||||
Self { repo: Rc::new(repo), notify: Rc::new(Default::default()) }
|
||||
}
|
||||
|
||||
fn toggle_done(&self, index: usize) {
|
||||
if !self.repo.toggle_done(index) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.notify.row_changed(index)
|
||||
}
|
||||
|
||||
fn remove_task(&self, index: usize) {
|
||||
if !self.repo.remove_task(index) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.notify.row_removed(index, 1)
|
||||
}
|
||||
|
||||
fn push_task(&self, task: mvc::TaskModel) {
|
||||
if !self.repo.push_task(task) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.notify.row_added(self.row_count() - 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
impl Model for TaskModel {
|
||||
type Data = mvc::TaskModel;
|
||||
|
||||
fn row_count(&self) -> usize {
|
||||
self.repo.task_count()
|
||||
}
|
||||
|
||||
fn row_data(&self, row: usize) -> Option<Self::Data> {
|
||||
self.repo.get_task(row)
|
||||
}
|
||||
|
||||
fn model_tracker(&self) -> &dyn ModelTracker {
|
||||
self.notify.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mvc;
|
||||
use std::cell::Cell;
|
||||
|
||||
fn test_controller() -> TaskListController {
|
||||
TaskListController::new(mvc::MockTaskRepository::new(vec![
|
||||
mvc::TaskModel { title: "Item 1".into(), due_date: 1, done: true },
|
||||
mvc::TaskModel { title: "Item 2".into(), due_date: 1, done: false },
|
||||
]))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tasks() {
|
||||
let controller = test_controller();
|
||||
let task_model = controller.task_model();
|
||||
|
||||
assert_eq!(task_model.row_count(), 2);
|
||||
assert_eq!(
|
||||
task_model.row_data(0),
|
||||
Some(mvc::TaskModel { title: "Item 1".into(), due_date: 1, done: true },)
|
||||
);
|
||||
assert_eq!(
|
||||
task_model.row_data(1),
|
||||
Some(mvc::TaskModel { title: "Item 2".into(), due_date: 1, done: false },)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_task_checked() {
|
||||
let controller = test_controller();
|
||||
let task_model = controller.task_model();
|
||||
|
||||
assert!(task_model.row_data(0).unwrap().done);
|
||||
controller.toggle_done(0);
|
||||
assert!(!task_model.row_data(0).unwrap().done);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_task() {
|
||||
let controller = test_controller();
|
||||
let task_model = controller.task_model();
|
||||
|
||||
assert_eq!(task_model.row_count(), 2);
|
||||
controller.remove_task(0);
|
||||
assert_eq!(task_model.row_count(), 1);
|
||||
|
||||
assert_eq!(
|
||||
task_model.row_data(0),
|
||||
Some(mvc::TaskModel { title: "Item 2".into(), due_date: 1, done: false },)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_show_create_task() {
|
||||
let controller = test_controller();
|
||||
|
||||
let callback_invoked = Rc::new(Cell::new(false));
|
||||
|
||||
controller.on_show_create_task({
|
||||
let callback_invoked = callback_invoked.clone();
|
||||
|
||||
move || {
|
||||
callback_invoked.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
controller.show_create_task();
|
||||
|
||||
assert!(callback_invoked.get());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_task() {
|
||||
let controller = test_controller();
|
||||
let task_model = controller.task_model();
|
||||
|
||||
assert_eq!(task_model.row_count(), 2);
|
||||
controller.create_task("Item 3", 3);
|
||||
assert_eq!(task_model.row_count(), 3);
|
||||
|
||||
assert_eq!(
|
||||
task_model.row_data(2),
|
||||
Some(mvc::TaskModel { title: "Item 3".into(), due_date: 3, done: false },)
|
||||
);
|
||||
}
|
||||
}
|
11
examples/todo-mvc/rust/src/mvc/models.rs
Normal file
11
examples/todo-mvc/rust/src/mvc/models.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
mod date_model;
|
||||
pub use date_model::*;
|
||||
|
||||
mod task_model;
|
||||
pub use task_model::*;
|
||||
|
||||
mod time_model;
|
||||
pub use time_model::*;
|
9
examples/todo-mvc/rust/src/mvc/models/date_model.rs
Normal file
9
examples/todo-mvc/rust/src/mvc/models/date_model.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, PartialEq)]
|
||||
pub struct DateModel {
|
||||
pub year: i32,
|
||||
pub month: u32,
|
||||
pub day: u32,
|
||||
}
|
11
examples/todo-mvc/rust/src/mvc/models/task_model.rs
Normal file
11
examples/todo-mvc/rust/src/mvc/models/task_model.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#[derive(Clone, Default, Debug, PartialEq)]
|
||||
pub struct TaskModel {
|
||||
pub title: String,
|
||||
|
||||
// due date in milliseconds
|
||||
pub due_date: i64,
|
||||
pub done: bool,
|
||||
}
|
9
examples/todo-mvc/rust/src/mvc/models/time_model.rs
Normal file
9
examples/todo-mvc/rust/src/mvc/models/time_model.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, PartialEq)]
|
||||
pub struct TimeModel {
|
||||
pub hour: u32,
|
||||
pub minute: u32,
|
||||
pub second: u32,
|
||||
}
|
32
examples/todo-mvc/rust/src/mvc/repositories.rs
Normal file
32
examples/todo-mvc/rust/src/mvc/repositories.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
mod mock_date_time_repository;
|
||||
pub use mock_date_time_repository::*;
|
||||
|
||||
mod mock_task_repository;
|
||||
pub use mock_task_repository::*;
|
||||
|
||||
use crate::mvc::models::{DateModel, TaskModel, TimeModel};
|
||||
|
||||
pub mod traits;
|
||||
|
||||
pub fn date_time_repo() -> impl traits::DateTimeRepository + Clone {
|
||||
MockDateTimeRepository::new(
|
||||
DateModel { year: 2024, month: 6, day: 11 },
|
||||
TimeModel { hour: 16, minute: 43, second: 0 },
|
||||
1718183634,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn task_repo() -> impl traits::TaskRepository + Clone {
|
||||
MockTaskRepository::new(vec![
|
||||
TaskModel { title: "Learn Rust".into(), done: true, due_date: 1717686537151 },
|
||||
TaskModel { title: "Learn Slint".into(), done: true, due_date: 1717686537151 },
|
||||
TaskModel {
|
||||
title: "Create project with Rust and Slint".into(),
|
||||
done: true,
|
||||
due_date: 1717686537151,
|
||||
},
|
||||
])
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::mvc;
|
||||
|
||||
use super::traits;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MockDateTimeRepository {
|
||||
current_date: mvc::DateModel,
|
||||
current_time: mvc::TimeModel,
|
||||
time_stamp: i32,
|
||||
}
|
||||
|
||||
impl MockDateTimeRepository {
|
||||
pub fn new(
|
||||
current_date: mvc::DateModel,
|
||||
current_time: mvc::TimeModel,
|
||||
time_stamp: i32,
|
||||
) -> Self {
|
||||
Self { current_date, current_time, time_stamp }
|
||||
}
|
||||
}
|
||||
|
||||
impl traits::DateTimeRepository for MockDateTimeRepository {
|
||||
fn current_date(&self) -> mvc::DateModel {
|
||||
self.current_date
|
||||
}
|
||||
|
||||
fn current_time(&self) -> mvc::TimeModel {
|
||||
self.current_time
|
||||
}
|
||||
|
||||
fn date_to_string(&self, date: mvc::DateModel) -> String {
|
||||
format!("{}/{}/{}", date.year, date.month, date.day)
|
||||
}
|
||||
|
||||
fn time_to_string(&self, time: mvc::TimeModel) -> String {
|
||||
format!("{}:{}", time.hour, time.minute)
|
||||
}
|
||||
|
||||
fn time_stamp(&self, _date: mvc::DateModel, _time: mvc::TimeModel) -> i32 {
|
||||
self.time_stamp
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use super::traits;
|
||||
use crate::mvc;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MockTaskRepository {
|
||||
tasks: Rc<RefCell<Vec<mvc::TaskModel>>>,
|
||||
}
|
||||
|
||||
impl MockTaskRepository {
|
||||
pub fn new(tasks: Vec<mvc::TaskModel>) -> Self {
|
||||
Self { tasks: Rc::new(RefCell::new(tasks)) }
|
||||
}
|
||||
}
|
||||
|
||||
impl traits::TaskRepository for MockTaskRepository {
|
||||
fn task_count(&self) -> usize {
|
||||
self.tasks.borrow().len()
|
||||
}
|
||||
|
||||
fn get_task(&self, index: usize) -> Option<mvc::TaskModel> {
|
||||
self.tasks.borrow().get(index).cloned()
|
||||
}
|
||||
|
||||
fn toggle_done(&self, index: usize) -> bool {
|
||||
if let Some(task) = self.tasks.borrow_mut().get_mut(index) {
|
||||
task.done = !task.done;
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn remove_task(&self, index: usize) -> bool {
|
||||
if index < self.tasks.borrow().len() {
|
||||
self.tasks.borrow_mut().remove(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn push_task(&self, task: mvc::TaskModel) -> bool {
|
||||
self.tasks.borrow_mut().push(task);
|
||||
true
|
||||
}
|
||||
}
|
8
examples/todo-mvc/rust/src/mvc/repositories/traits.rs
Normal file
8
examples/todo-mvc/rust/src/mvc/repositories/traits.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
mod date_time_repository;
|
||||
pub use date_time_repository::*;
|
||||
|
||||
mod task_repository;
|
||||
pub use task_repository::*;
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::mvc;
|
||||
|
||||
pub trait DateTimeRepository {
|
||||
fn current_date(&self) -> mvc::DateModel;
|
||||
fn current_time(&self) -> mvc::TimeModel;
|
||||
fn date_to_string(&self, date: mvc::DateModel) -> String;
|
||||
fn time_to_string(&self, time: mvc::TimeModel) -> String;
|
||||
fn time_stamp(&self, date: mvc::DateModel, time: mvc::TimeModel) -> i32;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::mvc;
|
||||
|
||||
pub trait TaskRepository {
|
||||
fn task_count(&self) -> usize;
|
||||
fn get_task(&self, index: usize) -> Option<mvc::TaskModel>;
|
||||
fn toggle_done(&self, index: usize) -> bool;
|
||||
fn remove_task(&self, index: usize) -> bool;
|
||||
fn push_task(&self, task: mvc::TaskModel) -> bool;
|
||||
}
|
8
examples/todo-mvc/rust/src/ui.rs
Normal file
8
examples/todo-mvc/rust/src/ui.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
slint::include_modules!();
|
||||
|
||||
pub mod create_task_adapter;
|
||||
pub mod navigation_adapter;
|
||||
pub mod task_list_adapter;
|
108
examples/todo-mvc/rust/src/ui/create_task_adapter.rs
Normal file
108
examples/todo-mvc/rust/src/ui/create_task_adapter.rs
Normal file
|
@ -0,0 +1,108 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use slint::*;
|
||||
|
||||
use crate::{
|
||||
mvc::{
|
||||
{CreateTaskController, TaskListController}, {DateModel, TimeModel},
|
||||
},
|
||||
ui,
|
||||
};
|
||||
|
||||
// a helper function to make adapter and controller connection a little bit easier
|
||||
fn connect_with_controller(
|
||||
view_handle: &ui::MainWindow,
|
||||
controller: &CreateTaskController,
|
||||
connect_adapter_controller: impl FnOnce(ui::CreateTaskAdapter, CreateTaskController) + 'static,
|
||||
) {
|
||||
connect_adapter_controller(view_handle.global::<ui::CreateTaskAdapter>(), controller.clone());
|
||||
}
|
||||
|
||||
// a helper function to make adapter and controller connection a little bit easier
|
||||
fn connect_with_task_list_controller(
|
||||
view_handle: &ui::MainWindow,
|
||||
controller: &TaskListController,
|
||||
connect_adapter_controller: impl FnOnce(ui::CreateTaskAdapter, TaskListController) + 'static,
|
||||
) {
|
||||
connect_adapter_controller(view_handle.global::<ui::CreateTaskAdapter>(), controller.clone());
|
||||
}
|
||||
|
||||
// one place to implement connection between adapter (view) and controller
|
||||
pub fn connect(view_handle: &ui::MainWindow, controller: CreateTaskController) {
|
||||
connect_with_controller(view_handle, &controller, {
|
||||
move |adapter, controller| {
|
||||
adapter.on_back(move || {
|
||||
controller.back();
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
connect_with_controller(view_handle, &controller, {
|
||||
move |adapter, controller| {
|
||||
adapter.on_current_date(move || map_date_model_to_date(controller.current_date()))
|
||||
}
|
||||
});
|
||||
|
||||
connect_with_controller(view_handle, &controller, {
|
||||
move |adapter, controller| {
|
||||
adapter.on_current_time(move || map_time_model_to_time(controller.current_time()))
|
||||
}
|
||||
});
|
||||
|
||||
connect_with_controller(view_handle, &controller, {
|
||||
move |adapter, controller| {
|
||||
adapter.on_date_string(move |date| {
|
||||
controller.date_string(map_date_to_date_model(date)).into()
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
connect_with_controller(view_handle, &controller, {
|
||||
move |adapter, controller| {
|
||||
adapter.on_time_string(move |time| {
|
||||
controller.time_string(map_time_to_time_model(time)).into()
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
connect_with_controller(view_handle, &controller, {
|
||||
move |adapter, controller| {
|
||||
adapter.on_time_stamp(move |date, time| {
|
||||
controller
|
||||
.time_stamp(map_date_to_date_model(date), map_time_to_time_model(time))
|
||||
.into()
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn connect_task_list_controller(view_handle: &ui::MainWindow, controller: TaskListController) {
|
||||
connect_with_task_list_controller(view_handle, &controller, {
|
||||
move |adapter, controller| {
|
||||
adapter.on_create(move |title, time_stamp| {
|
||||
controller.create_task(title.as_str(), time_stamp as i64)
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn map_time_model_to_time(time_model: TimeModel) -> ui::Time {
|
||||
ui::Time {
|
||||
hour: time_model.hour as i32,
|
||||
minute: time_model.minute as i32,
|
||||
second: time_model.second as i32,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_time_to_time_model(time: ui::Time) -> TimeModel {
|
||||
TimeModel { hour: time.hour as u32, minute: time.minute as u32, second: time.second as u32 }
|
||||
}
|
||||
|
||||
fn map_date_model_to_date(date_model: DateModel) -> ui::Date {
|
||||
ui::Date { year: date_model.year, month: date_model.month as i32, day: date_model.day as i32 }
|
||||
}
|
||||
|
||||
fn map_date_to_date_model(date: ui::Date) -> DateModel {
|
||||
DateModel { year: date.year, month: date.month as u32, day: date.day as u32 }
|
||||
}
|
34
examples/todo-mvc/rust/src/ui/navigation_adapter.rs
Normal file
34
examples/todo-mvc/rust/src/ui/navigation_adapter.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use slint::*;
|
||||
|
||||
use crate::{
|
||||
mvc::{CreateTaskController, TaskListController},
|
||||
ui,
|
||||
};
|
||||
|
||||
// one place to implement connection between adapter (view) and controller
|
||||
pub fn connect_create_task_controller(
|
||||
view_handle: &ui::MainWindow,
|
||||
controller: CreateTaskController,
|
||||
) {
|
||||
controller.on_back({
|
||||
let view_handle = view_handle.as_weak();
|
||||
|
||||
move || {
|
||||
view_handle.unwrap().global::<ui::NavigationAdapter>().invoke_previous_page();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// one place to implement connection between adapter (view) and controller
|
||||
pub fn connect_task_list_controller(view_handle: &ui::MainWindow, controller: TaskListController) {
|
||||
controller.on_show_create_task({
|
||||
let view_handle = view_handle.as_weak();
|
||||
|
||||
move || {
|
||||
view_handle.unwrap().global::<ui::NavigationAdapter>().invoke_next_page();
|
||||
}
|
||||
});
|
||||
}
|
66
examples/todo-mvc/rust/src/ui/task_list_adapter.rs
Normal file
66
examples/todo-mvc/rust/src/ui/task_list_adapter.rs
Normal file
|
@ -0,0 +1,66 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use chrono::DateTime;
|
||||
use slint::*;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::{
|
||||
mvc::{TaskListController, TaskModel},
|
||||
ui,
|
||||
};
|
||||
|
||||
// a helper function to make adapter and controller connection a little bit easier
|
||||
pub fn connect_with_controller(
|
||||
view_handle: &ui::MainWindow,
|
||||
controller: &TaskListController,
|
||||
connect_adapter_controller: impl FnOnce(ui::TaskListAdapter, TaskListController) + 'static,
|
||||
) {
|
||||
connect_adapter_controller(view_handle.global::<ui::TaskListAdapter>(), controller.clone());
|
||||
}
|
||||
|
||||
// one place to implement connection between adapter (view) and controller
|
||||
pub fn connect(view_handle: &ui::MainWindow, controller: TaskListController) {
|
||||
// sets a mapped list of the task items to the ui
|
||||
view_handle
|
||||
.global::<ui::TaskListAdapter>()
|
||||
.set_tasks(Rc::new(MapModel::new(controller.task_model(), map_task_to_item)).into());
|
||||
|
||||
connect_with_controller(view_handle, &controller, {
|
||||
move |adapter, controller| {
|
||||
adapter.on_toggle_task_checked(move |index| {
|
||||
controller.toggle_done(index as usize);
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
connect_with_controller(view_handle, &controller, {
|
||||
move |adapter, controller| {
|
||||
adapter.on_remove_task(move |index| {
|
||||
controller.remove_task(index as usize);
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
connect_with_controller(view_handle, &controller, {
|
||||
move |adapter: ui::TaskListAdapter, controller| {
|
||||
adapter.on_show_create_task(move || {
|
||||
controller.show_create_task();
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// maps a TaskModel (data) to a SelectionItem (ui)
|
||||
fn map_task_to_item(task: TaskModel) -> ui::SelectionListViewItem {
|
||||
ui::SelectionListViewItem {
|
||||
text: task.title.into(),
|
||||
checked: task.done,
|
||||
description: DateTime::from_timestamp_millis(task.due_date)
|
||||
.unwrap()
|
||||
// example: Thu, Jun 6, 2024 16:29
|
||||
.format("%a, %b %d, %Y %H:%M")
|
||||
.to_string()
|
||||
.into(),
|
||||
}
|
||||
}
|
47
examples/todo-mvc/ui/index.slint
Normal file
47
examples/todo-mvc/ui/index.slint
Normal file
|
@ -0,0 +1,47 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { TaskListView, TaskListAdapter } from "./views/task_list_view.slint";
|
||||
export { TaskListAdapter }
|
||||
|
||||
import { CreateTaskView, CreateTaskAdapter } from "./views/create_task_view.slint";
|
||||
export { CreateTaskAdapter }
|
||||
|
||||
import { AnimationSettings } from "./widgets/styling.slint";
|
||||
|
||||
export global NavigationAdapter {
|
||||
out property <int> current-page;
|
||||
|
||||
public function next-page() {
|
||||
root.current-page += 1;
|
||||
}
|
||||
|
||||
public function previous-page() {
|
||||
root.current-page = max(0, root.current-page - 1);
|
||||
}
|
||||
}
|
||||
|
||||
export component MainWindow inherits Window {
|
||||
preferred-width: 400px;
|
||||
preferred-height: 600px;
|
||||
title: "Slint todo mvc example";
|
||||
|
||||
layout := HorizontalLayout {
|
||||
x: -(NavigationAdapter.current-page * root.width);
|
||||
|
||||
TaskListView {
|
||||
width: root.width;
|
||||
height: root.height;
|
||||
}
|
||||
|
||||
CreateTaskView {
|
||||
width: root.width;
|
||||
height: root.height;
|
||||
}
|
||||
|
||||
animate x {
|
||||
duration: AnimationSettings.move-duration;
|
||||
easing: AnimationSettings.move-easing;
|
||||
}
|
||||
}
|
||||
}
|
159
examples/todo-mvc/ui/views/create_task_view.slint
Normal file
159
examples/todo-mvc/ui/views/create_task_view.slint
Normal file
|
@ -0,0 +1,159 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { Date, Time, LineEdit, TimePicker, DatePicker, VerticalBox, Button } from "std-widgets.slint";
|
||||
import { IconButton } from "../widgets/icon_button.slint";
|
||||
import { TextButton } from "../widgets/text_button.slint";
|
||||
import { Icons, FontSettings, TodoPalette, SpaceSettings } from "../widgets/styling.slint";
|
||||
|
||||
export global CreateTaskAdapter {
|
||||
in-out property <Date> due-date: { year: 2024, month: 12, day: 24 };
|
||||
in-out property <Time> due-time: { hour: 12, minute: 45, second: 0 };
|
||||
|
||||
callback create(/* title */ string, /* due-date-time */ int);
|
||||
callback back();
|
||||
pure callback date-string(Date) -> string;
|
||||
pure callback time-string(Time) -> string;
|
||||
pure callback current-date() -> Date;
|
||||
pure callback current-time() -> Time;
|
||||
pure callback time-stamp(/* date */ Date, /* time */ Time) -> int;
|
||||
|
||||
// dummy implementation for live preview
|
||||
date-string(due-date) => {
|
||||
"Sat, Jun 1, 2024"
|
||||
}
|
||||
|
||||
// dummy implementation for live preview
|
||||
time-string(due-time) => {
|
||||
"09:00"
|
||||
}
|
||||
}
|
||||
|
||||
export component CreateTaskView {
|
||||
VerticalBox {
|
||||
HorizontalLayout {
|
||||
IconButton {
|
||||
icon: Icons.close;
|
||||
|
||||
clicked => {
|
||||
root.reset();
|
||||
CreateTaskAdapter.back();
|
||||
}
|
||||
}
|
||||
|
||||
// spacer
|
||||
Rectangle {}
|
||||
|
||||
Button {
|
||||
text: @tr("DONE");
|
||||
enabled: title-input.text != "";
|
||||
primary: true;
|
||||
|
||||
clicked => {
|
||||
root.done();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VerticalLayout {
|
||||
spacing: SpaceSettings.default-spacing;
|
||||
|
||||
Text {
|
||||
text: @tr("Task name");
|
||||
color: TodoPalette.foreground;
|
||||
font-size: FontSettings.body-strong.font-size;
|
||||
font-weight: FontSettings.body-strong.font-weight;
|
||||
horizontal-alignment: left;
|
||||
overflow: elide;
|
||||
}
|
||||
|
||||
title-input := LineEdit {
|
||||
placeholder-text: @tr("Describe your task");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Text {
|
||||
text: @tr("Due date");
|
||||
color: TodoPalette.foreground;
|
||||
font-size: FontSettings.body-strong.font-size;
|
||||
font-weight: FontSettings.body-strong.font-weight;
|
||||
horizontal-alignment: left;
|
||||
overflow: elide;
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
spacing: SpaceSettings.default-spacing;
|
||||
|
||||
TextButton {
|
||||
text: CreateTaskAdapter.date-string(CreateTaskAdapter.due-date);
|
||||
|
||||
clicked => {
|
||||
date-picker.show();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TextButton {
|
||||
text: CreateTaskAdapter.time-string(CreateTaskAdapter.due-time);
|
||||
horizontal-stretch: 0;
|
||||
|
||||
clicked => {
|
||||
time-picker.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {}
|
||||
}
|
||||
|
||||
date-picker := PopupWindow {
|
||||
x: (root.width - 360px) / 2;
|
||||
y: (root.height - 524px) / 2;
|
||||
width: 360px;
|
||||
height: 524px;
|
||||
close-on-click: false;
|
||||
|
||||
DatePicker {
|
||||
canceled => {
|
||||
date-picker.close();
|
||||
}
|
||||
|
||||
accepted(date) => {
|
||||
CreateTaskAdapter.due-date = date;
|
||||
date-picker.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
time-picker := PopupWindow {
|
||||
x: (root.width - 340px) / 2;
|
||||
y: (root.height - 500px) / 2;
|
||||
width: 340px;
|
||||
height: 500px;
|
||||
close-on-click: false;
|
||||
|
||||
TimePicker {
|
||||
canceled => {
|
||||
time-picker.close();
|
||||
}
|
||||
|
||||
accepted(time) => {
|
||||
CreateTaskAdapter.due-time = time;
|
||||
time-picker.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
title-input.text = "";
|
||||
CreateTaskAdapter.due-date = CreateTaskAdapter.current-date();
|
||||
CreateTaskAdapter.due-time = CreateTaskAdapter.current-time();
|
||||
}
|
||||
|
||||
function done() {
|
||||
CreateTaskAdapter.back();
|
||||
CreateTaskAdapter.create(title-input.text, CreateTaskAdapter.time-stamp(CreateTaskAdapter.due-date, CreateTaskAdapter.due-time));
|
||||
root.reset();
|
||||
}
|
||||
}
|
71
examples/todo-mvc/ui/views/task_list_view.slint
Normal file
71
examples/todo-mvc/ui/views/task_list_view.slint
Normal file
|
@ -0,0 +1,71 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { VerticalBox } from "std-widgets.slint";
|
||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { SelectionListView, SelectionListViewItem } from "../widgets/selection_list_view.slint";
|
||||
export { SelectionListViewItem }
|
||||
|
||||
import { ActionButton } from "../widgets/action_button.slint";
|
||||
import { Icons } from "../widgets/styling.slint";
|
||||
|
||||
export global TaskListAdapter {
|
||||
in-out property <[SelectionListViewItem]> tasks: [
|
||||
// this is only dummy data for the preview
|
||||
{ text: "Contribute to Slint", description: "2024/11/11 13:13" },
|
||||
{ text: "Open a discussion on GitHub", description: "2024/11/11 13:13" },
|
||||
{ text: "Write some documentation", description: "2024/11/11 13:13" }
|
||||
];
|
||||
|
||||
callback toggle-task-checked(/* index */ int);
|
||||
callback remove-task(/* index */ int);
|
||||
callback show-create-task();
|
||||
|
||||
// this is only a dummy implementation for the preview
|
||||
toggle-task-checked(index) => {
|
||||
root.tasks[index] = {
|
||||
text: root.item(index).text,
|
||||
checked: !root.item(index).checked,
|
||||
description: root.item(index).description
|
||||
};
|
||||
}
|
||||
|
||||
function item(index: int) -> SelectionListViewItem {
|
||||
root.tasks[index]
|
||||
}
|
||||
}
|
||||
|
||||
export component TaskListView {
|
||||
VerticalBox {
|
||||
padding-top: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
|
||||
SelectionListView {
|
||||
width: 100%;
|
||||
model: TaskListAdapter.tasks;
|
||||
|
||||
toggle(index) => {
|
||||
TaskListAdapter.toggle-task-checked(index);
|
||||
}
|
||||
|
||||
remove(index) => {
|
||||
TaskListAdapter.remove-task(index);
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
alignment: center;
|
||||
|
||||
ActionButton {
|
||||
icon: Icons.add;
|
||||
|
||||
clicked => {
|
||||
TaskListAdapter.show-create-task();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
54
examples/todo-mvc/ui/widgets/action_button.slint
Normal file
54
examples/todo-mvc/ui/widgets/action_button.slint
Normal file
|
@ -0,0 +1,54 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { SizeSettings, TodoPalette } from "styling.slint";
|
||||
import { FocusTouchArea } from "focus_touch_area.slint";
|
||||
import { StateLayer } from "./state_layer.slint";
|
||||
|
||||
export component ActionButton {
|
||||
callback clicked;
|
||||
|
||||
in property <image> icon;
|
||||
|
||||
horizontal-stretch: 0;
|
||||
vertical-stretch: 0;
|
||||
forward-focus: touch-area;
|
||||
width: self.height;
|
||||
height: SizeSettings.control-big-height;
|
||||
|
||||
touch-area := FocusTouchArea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
clicked => {
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
|
||||
background-layer := Rectangle {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: TodoPalette.accent-background;
|
||||
border-radius: self.height / 2;
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: background-layer.border-radius;
|
||||
pressed: touch-area.pressed || touch-area.enter-pressed;
|
||||
has-focus: touch-area.has-focus;
|
||||
has-hover: touch-area.has-hover;
|
||||
}
|
||||
|
||||
content-layer := HorizontalLayout {
|
||||
alignment: center;
|
||||
|
||||
icon-image := Image {
|
||||
source: root.icon;
|
||||
height: SizeSettings.control-icon-big-height;
|
||||
y: (parent.height - self.height) / 2;
|
||||
colorize: TodoPalette.accent-foreground;
|
||||
}
|
||||
}
|
||||
}
|
53
examples/todo-mvc/ui/widgets/focus_touch_area.slint
Normal file
53
examples/todo-mvc/ui/widgets/focus_touch_area.slint
Normal file
|
@ -0,0 +1,53 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
export component FocusTouchArea {
|
||||
in property <bool> enabled: true;
|
||||
out property <bool> has-focus <=> focus-scope.has-focus;
|
||||
out property <bool> pressed <=> touch-area.pressed;
|
||||
out property <bool> has-hover <=> touch-area.has-hover;
|
||||
out property <bool> enter-pressed;
|
||||
|
||||
in property <MouseCursor> mouse-cursor <=> touch-area.mouse-cursor;
|
||||
|
||||
callback clicked <=> touch-area.clicked;
|
||||
|
||||
forward-focus: focus-scope;
|
||||
|
||||
focus-scope := FocusScope {
|
||||
x: 0;
|
||||
width: 0px;
|
||||
enabled: root.enabled;
|
||||
|
||||
key-pressed(event) => {
|
||||
if !root.enabled {
|
||||
return reject;
|
||||
}
|
||||
|
||||
if (event.text == " " || event.text == "\n") && !root.enter-pressed {
|
||||
root.enter-pressed = true;
|
||||
touch-area.clicked();
|
||||
return accept;
|
||||
}
|
||||
|
||||
reject
|
||||
}
|
||||
|
||||
key-released(event) => {
|
||||
if !root.enabled {
|
||||
return reject;
|
||||
}
|
||||
|
||||
if (event.text == " " || event.text == "\n") && root.enter-pressed {
|
||||
root.enter-pressed = false;
|
||||
return accept;
|
||||
}
|
||||
|
||||
reject
|
||||
}
|
||||
}
|
||||
|
||||
touch-area := TouchArea {
|
||||
enabled: root.enabled;
|
||||
}
|
||||
}
|
47
examples/todo-mvc/ui/widgets/icon_button.slint
Normal file
47
examples/todo-mvc/ui/widgets/icon_button.slint
Normal file
|
@ -0,0 +1,47 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { SizeSettings, TodoPalette } from "styling.slint";
|
||||
import { FocusTouchArea } from "focus_touch_area.slint";
|
||||
import { StateLayer } from "./state_layer.slint";
|
||||
|
||||
export component IconButton {
|
||||
callback clicked;
|
||||
|
||||
in property <image> icon;
|
||||
|
||||
horizontal-stretch: 0;
|
||||
vertical-stretch: 0;
|
||||
forward-focus: touch-area;
|
||||
width: self.height;
|
||||
height: SizeSettings.control-height;
|
||||
|
||||
touch-area := FocusTouchArea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
clicked => {
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: self.height / 2;
|
||||
pressed: touch-area.pressed || touch-area.enter-pressed;
|
||||
has-focus: touch-area.has-focus;
|
||||
has-hover: touch-area.has-hover;
|
||||
}
|
||||
|
||||
content-layer := HorizontalLayout {
|
||||
alignment: center;
|
||||
|
||||
icon-image := Image {
|
||||
source: root.icon;
|
||||
height: SizeSettings.control-icon-height;
|
||||
y: (parent.height - self.height) / 2;
|
||||
colorize: TodoPalette.foreground;
|
||||
}
|
||||
}
|
||||
}
|
108
examples/todo-mvc/ui/widgets/selection_list_view.slint
Normal file
108
examples/todo-mvc/ui/widgets/selection_list_view.slint
Normal file
|
@ -0,0 +1,108 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { CheckBox, ListView, HorizontalBox } from "std-widgets.slint";
|
||||
import { StateLayer } from "./state_layer.slint";
|
||||
import { FocusTouchArea } from "./focus_touch_area.slint";
|
||||
import { SizeSettings, TodoPalette, FontSettings, Icons } from "styling.slint";
|
||||
import { IconButton } from "icon_button.slint";
|
||||
|
||||
@rust-attr(derive(serde::Serialize, serde::Deserialize))
|
||||
export struct SelectionListViewItem {
|
||||
text: string,
|
||||
checked: bool,
|
||||
description: string,
|
||||
}
|
||||
|
||||
export component SelectionListViewItemDelegate {
|
||||
callback toggle;
|
||||
callback remove;
|
||||
|
||||
in property <string> text <=> text-label.text;
|
||||
in property <string> description <=> description-label.text;
|
||||
in-out property <bool> checked <=> check-box.checked;
|
||||
|
||||
min-width: content-layer.min-width;
|
||||
min-height: max(SizeSettings.control-height, content-layer.min-height);
|
||||
forward-focus: touch-area;
|
||||
|
||||
touch-area := FocusTouchArea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
clicked => {
|
||||
root.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
focus-padding: -1px;
|
||||
pressed: touch-area.pressed || touch-area.enter-pressed;
|
||||
has-focus: touch-area.has-focus;
|
||||
has-hover: touch-area.has-hover;
|
||||
}
|
||||
|
||||
content-layer := HorizontalBox {
|
||||
check-box := CheckBox {
|
||||
horizontal-stretch: 0;
|
||||
y: (parent.height - self.height) / 2;
|
||||
|
||||
toggled => {
|
||||
root.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
|
||||
text-label := Text {
|
||||
horizontal-alignment: left;
|
||||
color: TodoPalette.foreground;
|
||||
font-size: FontSettings.body-strong.font-size;
|
||||
font-weight: FontSettings.body-strong.font-weight;
|
||||
overflow: elide;
|
||||
}
|
||||
|
||||
description-label := Text {
|
||||
color: TodoPalette.foreground;
|
||||
font-size: FontSettings.body.font-size;
|
||||
font-weight: FontSettings.body.font-weight;
|
||||
overflow: elide;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
IconButton {
|
||||
y: (parent.height - self.height) / 2;
|
||||
icon: Icons.remove;
|
||||
|
||||
clicked => {
|
||||
root.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component SelectionListView inherits ListView {
|
||||
in property <[SelectionListViewItem]> model;
|
||||
|
||||
callback toggle(/* index */ int);
|
||||
callback remove(/* index */ int);
|
||||
|
||||
for item[index] in root.model : SelectionListViewItemDelegate {
|
||||
width: root.visible-width;
|
||||
text: item.text;
|
||||
description: item.description;
|
||||
checked: item.checked;
|
||||
|
||||
toggle => {
|
||||
root.toggle(index);
|
||||
}
|
||||
|
||||
remove => {
|
||||
root.remove(index);
|
||||
}
|
||||
}
|
||||
}
|
54
examples/todo-mvc/ui/widgets/state_layer.slint
Normal file
54
examples/todo-mvc/ui/widgets/state_layer.slint
Normal file
|
@ -0,0 +1,54 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { TodoPalette, AnimationSettings } from "styling.slint";
|
||||
|
||||
export component StateLayer {
|
||||
// styling
|
||||
in property <length> border-radius <=> state-layer.border-radius;
|
||||
|
||||
// states
|
||||
in property <bool> pressed;
|
||||
in property <bool> has-hover;
|
||||
in property <bool> has-focus;
|
||||
in property <length> focus-padding: 2px;
|
||||
|
||||
focus-border := Rectangle {
|
||||
x: (root.width - self.width) / 2;
|
||||
y: (root.height - self.height) / 2;
|
||||
width: root.width + 2 * root.focus-padding;
|
||||
height: root.height + 2 * root.focus-padding;
|
||||
border-radius: state-layer.border-radius + root.focus-padding;
|
||||
border-width: 1px;
|
||||
border-color: TodoPalette.focus-border;
|
||||
opacity: 0;
|
||||
|
||||
states [
|
||||
focused when root.has-focus : {
|
||||
opacity: 1;
|
||||
}
|
||||
]
|
||||
|
||||
animate opacity {
|
||||
duration: AnimationSettings.color-duration;
|
||||
}
|
||||
}
|
||||
|
||||
state-layer := Rectangle {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
animate background {
|
||||
duration: AnimationSettings.color-duration;
|
||||
}
|
||||
}
|
||||
|
||||
states [
|
||||
pressed when root.pressed : {
|
||||
state-layer.background: TodoPalette.state-pressed;
|
||||
}
|
||||
hoverd when root.has-hover: {
|
||||
state-layer.background: TodoPalette.state-hovered;
|
||||
}
|
||||
]
|
||||
}
|
58
examples/todo-mvc/ui/widgets/styling.slint
Normal file
58
examples/todo-mvc/ui/widgets/styling.slint
Normal file
|
@ -0,0 +1,58 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { Palette } from "std-widgets.slint";
|
||||
|
||||
export global AnimationSettings {
|
||||
out property <duration> color-duration: 200ms;
|
||||
out property <duration> move-duration: 400ms;
|
||||
out property <easing> move-easing: cubic-bezier(0.3, 0.0, 0.8, 0.15);
|
||||
}
|
||||
|
||||
export global TodoPalette {
|
||||
out property <brush> foreground: Palette.foreground;
|
||||
out property <brush> accent-background: Palette.accent-background;
|
||||
out property <brush> accent-foreground: Palette.accent-foreground;
|
||||
out property <brush> focus-border: Palette.accent-background;
|
||||
out property <brush> state-pressed: root.dark-color-scheme ? #ffffff.with-alpha(0.3) : #000000.with-alpha(0.3);
|
||||
out property <brush> state-hovered: root.dark-color-scheme ? #ffffff.with-alpha(0.2) : #000000.with-alpha(0.2);
|
||||
|
||||
property <bool> dark-color-scheme: Palette.color-scheme == ColorScheme.dark;
|
||||
}
|
||||
|
||||
export struct TextStyle {
|
||||
font-size: relative-font-size,
|
||||
font-weight: int,
|
||||
}
|
||||
|
||||
export global FontSettings {
|
||||
out property <int> light-font-weight: 300;
|
||||
out property <int> regular-font-weight: 500;
|
||||
out property <int> semi-bold-font-weight: 600;
|
||||
out property <TextStyle> body: {
|
||||
font-size: 14 * 0.0769rem,
|
||||
font-weight: root.light-font-weight,
|
||||
};
|
||||
out property <TextStyle> body-strong: {
|
||||
font-size: 16 * 0.0769rem,
|
||||
font-weight: root.semi-bold-font-weight,
|
||||
};
|
||||
}
|
||||
|
||||
export global SizeSettings {
|
||||
out property <length> control-icon-height: 16px;
|
||||
out property <length> control-icon-big-height: 32px;
|
||||
out property <length> control-height: 32px;
|
||||
out property <length> control-big-height: 48px;
|
||||
}
|
||||
|
||||
export global SpaceSettings {
|
||||
out property <length> default-spacing: 4px;
|
||||
out property <length> default-padding: 8px;
|
||||
}
|
||||
|
||||
export global Icons {
|
||||
out property <image> add: @image-url("../../assets/add.svg");
|
||||
out property <image> remove: @image-url("../../assets/remove.svg");
|
||||
out property <image> close: @image-url("../../assets/close.svg");
|
||||
}
|
45
examples/todo-mvc/ui/widgets/text_button.slint
Normal file
45
examples/todo-mvc/ui/widgets/text_button.slint
Normal file
|
@ -0,0 +1,45 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { SizeSettings, SpaceSettings, FontSettings, TodoPalette } from "styling.slint";
|
||||
import { FocusTouchArea } from "focus_touch_area.slint";
|
||||
import { StateLayer } from "./state_layer.slint";
|
||||
|
||||
export component TextButton {
|
||||
callback clicked;
|
||||
|
||||
in property <string> text;
|
||||
|
||||
vertical-stretch: 0;
|
||||
forward-focus: touch-area;
|
||||
min-width: content-layer.min-width;
|
||||
min-height: max(content-layer.min-height, SizeSettings.control-height);
|
||||
|
||||
touch-area := FocusTouchArea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
clicked => {
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pressed: touch-area.pressed || touch-area.enter-pressed;
|
||||
has-focus: touch-area.has-focus;
|
||||
has-hover: touch-area.has-hover;
|
||||
}
|
||||
|
||||
content-layer := HorizontalLayout {
|
||||
Text {
|
||||
text: root.text;
|
||||
horizontal-alignment: left;
|
||||
vertical-alignment: center;
|
||||
font-size: FontSettings.body.font-size;
|
||||
font-weight: FontSettings.body.font-weight;
|
||||
color: TodoPalette.foreground;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue