Зачем еще один калькулятор? Да незачем, просто как тестовый проект для рассмотрения GUI-библиотеки.

Изначально я хотел попробовать такие крейты, как GPUI, Floem и Xilem, но первая, кажется, пока работает только под MacOS и Linux, вторая не позволяет установить иконку окну и кушает оперативы побольше Webview в Tauri, а до третьей я так и не добрался, узнав об Slint.

Об Slint есть всего несколько новостных постов на Хабре, поэтому, возможно, вам будет интересно посмотреть, что это такое.

Об Slint

Slint - это GUI-фреймворк, который, по заявлениям разработчиков, выделяется своей компактностью и легкостью использования. Сравниваясь с конкурентами, разработчики Slint утверждают, что их рантайм занимает всего 300 кб в оперативной памяти, что делает его идеальным выбором для embedded-систем.

Фреймворк официально поддерживает Rust, C++ и Node.js, и предоставляет собственный DSL, который затем компилируется в нативный код перечисленных языков и платформы. DSL представляет из себя некую смесь CSS и SwiftUI, что делает точку входа значительно ниже.

Разработчики проекта предоставляют плагины для популярных и не очень IDE и редакторов кода, который даёт нам щепотку intellisense и немного кривой превью, который не может нормально отработать свойства default-font-*. Рекомендую к этому накатить ещё Codeium, у них есть бета-поддержка Slint DSL (хотя на чудо рассчитывать не нужно).

Slint распространяется под несколькими лицензиями.

Создаём проект

Для тех, кто мало знаком с Rust, буду всё подробно расписывать (ну, установить-то инфраструктуру языка, наверное, и без меня сможете, да? Да, ведь?...). Поэтому... создаём проект! Это делается следующими командами:

cargo new
# или
cargo init

По сути, обе команды делают одно и то же, но для cargo new нужно обязательно указать имя новой директории. Но если для cargo init не указать название, она создаст проект в текущей директории.

Если Вам не нужен локальный репозиторий, который создаётся при инициализации проекта, стоит использовать флаг --vcs none.

В нашем случае проект будет носить название calc-rs. Воспользовавшись командой, получаем проект со следующей структурой:

Структура проекта
Структура проекта

Зависимости проекта

Добавим зависимости проекта. Для этого будем использовать плагин cargo-edit. Плагин можно установить командой:

cargo install cargo-edit

Теперь может добавлять зависимости следующим образом:

cargo add slint                                            # GUI
cargo add --build slint-build                              # Поддержка файлов DSL

Slint, или православный GUI

Slint предоставляет собственный DSL. Его можно использовать через макрос slint::slint!. И это довольно удобно, ведь можно в одном файле описывать и логику, и стили. Однако мы будем рассматривать случай с отдельными файлами.

domain-specific language (DSL) is a computer language specialized to a particular application domain. This is in contrast to a general-purpose language (GPL), which is broadly applicable across domains.

Или по нашенски:

Предметно-ориентированный язык (DSL) - это компьютерный язык, специализированный для конкретной прикладной области. В отличие от языка общего назначения (GPL), который широко применяется в разных областях.

Источник: wikipedia.org. Статья довольно исчерпывающая, поэтому не надо в меня тапками кидать за ссылку на вики.

Для этого создадим папку components, в которой создадим файл app.slint, в котором для начала определим простой счётчик.

import { VerticalBox, HorizontalBox, Button } from "std-widgets.slint";
export component AppWindow inherits Window {
    in property <string> window_title;

    width: 400px;
    height: 500px;
    title: window_title;
    default-font-size: 20px;

    property <int> count: 0;

    VerticalBox {
        alignment: center;
    
        Text {
            font-size: 30px;
            horizontal-alignment: center;
            text: "Count: " + count;
        }
        HorizontalBox {
            Button {
                text: "add";
                clicked => {
                    root.count += 1;
                }
            }
            Button {
                text: "subtract";
                clicked => {
                    if root.count != 0 {
                        root.count -= 1;
                    }
                }
            }
        }
    }
}

Для вёрстки есть такие layout, как VerticalLayout, HorizontalLayout и GridLayout, которые не требуют никакого импорта. И вы могли заметить, что используются VerticalBox и HorizontalBox, которые импортируются из стандартной библиотеки компонентов. Вся разница в том, что у компонентов из стандартной библиотеки предопределены отступы между контентом по типу тех, которые веб-разработчики постоянно обнуляют.

В корне проекта создаём файл build.rs, являющийся скриптом сборки, и добавляем в него функцию main с вызовом функции compile. Теперь app.slint – точка входа, больше ничего не нужно указывать. Эта функция компилирует DSL в код Rust. То есть ранее описанный компонент AppWindow будет скомпилирован в struct AppWindow.

fn main() {
    slint_build::compile("components/app.slint").unwrap();
}

Далее вызываем макрос slint::include_modules!() в файле src/main.rs и теперь можем инициализировать экземпляр нашего AppWindow.

use slint::SharedString;

slint::include_modules!();

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let app = AppWindow::new()?;
    app.set_window_title(SharedString::from("Counter"));
    app.run()?;

    Ok(())
}

Метод set_window_title был сгенерирован из in property window_title, описанный в файле components/app.slint. Спецификатор in делает доступным для записи свойство windows_title, и если его убрать, то он будет считаться приватным и метод set_window_title, как и метод get_window_title, не будет сгенерирован. Выдержка из документации на счёт спецификаторов доступа:

Добавьте к дополнительным свойствам спецификатор, который указывает, как это свойство может быть прочитано и записано в файл:

private (по умолчанию): Доступ к этому свойству возможен только из компонента.

in: Свойство является входным. Оно может быть установлено и изменено пользователем этого компонента, например, с помощью привязок или путем назначения в обратных вызовах. Компонент может предоставлять привязку по умолчанию, но не может перезаписывать ее с помощью назначения.

out: Выходное свойство, которое может быть установлено только компонентом. Оно доступно только для чтения пользователями компонентов.

in-out: Это свойство может быть прочитано и изменено кем угодно.

Источник: slint.dev.

Не совсем понял смысл спецификатора in-out, если есть in, делающий ровно то же самое ಠ_ರೃ. Прошу знающих написать в комментариях об этом подробнее.

cargo run – получаем на выходе наш счётчик-недокалькулятор:

"Wake the F*** up, Samurai. We have a city to burn."
"Wake the F*** up, Samurai. We have a city to burn."

Забавно то, что если вы случайно нажали Tab, то вряд ли уже получится снять фокус с кнопок без переопределения (ㆆ_ㆆ).

Мультиязычность

Slint из коробки поддерживает интернационализацию Gettext через макросы, что, как по-мне, такое себе. Почему? – я часа три пытался заставить его работать, но я так и не понял, почему ни этот модуль, ни модуль логов Slint так и не заработали, а в доках чуть ли не пару слов о них написано (╯°□°)╯︵ ┻━┻. Если кому будет интересно, сохраню версию с gettext в соответствующей ветке репы.

Поэтому будем использовать Fluent от Mozilla (●'◡'●). Fluent – другая система локализации, устраняющая некоторые проблемы своего предшественника gettext. Некоторые из этих проблем:

  1. Идентификатор сообщения. В gettext используется исходная строка (обычно на английском) в качестве идентификатора. Это накладывает ограничения на разработчиков, вынуждая их никогда не изменять исходные сообщения, поскольку это потребует обновления всех переводов.

  1. Варианты сообщений. Gettext поддерживает небольшой набор функций для интернационализации, в частности — для множественных чисел. Fluent поддерживает базовую концепцию вариативности строк, которую можно использовать с селекторами.

  1. Внешние аргументы. Gettext не поддерживает внешние аргументы, то есть нельзя задать форматирование параметров — чисел, дат. Fluent поддерживает внешние аргументы и позволяет локализаторам создавать более точные тексты для конкретных случаев.

  1. Аннулирование перевода. Gettext объединяет все три уровня в одно состояние под названием нечёткое совпадение (fuzzy). Во Fluent использование уникальных идентификаторов позволяет держать два из этих уровней отдельно от третьего.

  1. Формат данных. Gettext использует три формата файлов — *.po, *.pot и *.mo. Fluent использует единый формат файла *.ftl, что упрощает внедрение и не требует дополнительных шагов, которые могут привести к расхождению в данных.

    Источник: репозиторий Fluent Project.

Мем оттуда же:

Gettext поддерживает UTF-8. В целом, на этом поддержка Unicode заканчивается.

Хотя Fluent и выглядит круче с точки зрения локализаторов (создал нужный файлик и работаешь, без надобности переводить в разные форматы и компилировать), но вот официальные крейты от Fluent Project – довольно сырая шляпа. Поэтому будем работать через функции-костыли без излишеств (асинхронность, кэширование, сравнение локалей, продвинутая обработка ошибок и т.п.).

Реализация вспомогательных функций

Новые зависимости:

cargo add thiserror fluent sys-locale unic-langid
cargo add --build copy_to_output

src/file.rs:

use std::{env, path::{Path, PathBuf}};

use thiserror::Error;

#[derive(Error, Debug)]
pub enum FileReadError {
    #[error("Unable to read file: {0}")]
    ReadError(String, String),
    #[error("Unable to read file as UTF-8")]
    Utf8Error
}

/// Возвращает относительный путь к корневой папке проекта
pub fn get_dir_path<T: AsRef<Path>>(subfolder: T) -> PathBuf {
    let folder = subfolder.as_ref();
    get_root_dir().join(folder)
}

/// Возвращает абсолютный путь к корневой папке проекта, в котором расположен исполняемый файл программы
pub fn get_root_dir() -> PathBuf {
    env::current_exe()
        .expect("Unable to get a exe path.")
        .parent()
        .expect("Unable to get a root folder.")
        .into()
}

/// Считывает содержимое файлов
pub fn get_file_text(path: impl AsRef<Path>) -> Result<String, FileReadError> {
    let buf = match std::fs::read(&path) {
        Ok(buf) => buf,
        Err(e) => return Err(FileReadError::ReadError(path.as_ref().to_string_lossy().into_owned(), e.to_string()))
    };

    let text = match String::from_utf8(buf) {
        Ok(text) => text,
        Err(_) => return Err(FileReadError::Utf8Error)
    };

    Ok(text)
}

pub fn file_exists(path: impl AsRef<Path>) -> bool {
    std::fs::metadata(path).is_ok()
}

src/fluent.rs:

use std::{fmt::Debug, rc::Rc};
use fluent::{FluentArgs, FluentBundle, FluentResource};

use crate::file::{file_exists, get_file_text};

#[derive(thiserror::Error, Debug)]
pub enum FluentError {
    #[error("Unable to read FTL file: {0}")]
    UnableReadFtlError(#[from] crate::file::FileReadError),

    #[error("Fluent syntax error")]
    FluentSyntaxError,

    #[error("Message by key {0} not found")]
    MessageNotFoundError(String)
}

#[inline]
pub fn get_locale() -> String {
    if let Some(locale) = sys_locale::get_locale() {
        locale
    } else {
        default_locale()
    }
}

#[inline]
fn default_locale() -> String {
    "en-US".to_string()
}

pub fn get_msg(bundle: &Bundle, key: impl AsRef<str>+Debug, args: Option<&FluentArgs>) -> Result<String, FluentError> {
    let key = key.as_ref();

    if let Some(fluent_msg) = bundle.get_message(key) {
        if let Some(pattern) = fluent_msg.value() {
            let mut errors = Vec::new();
            let msg = bundle.format_pattern(pattern, args, &mut errors);
            
            for e in errors {
                eprintln!("{}", e);
            }
            
            Ok(msg.into_owned())
        } else {
            Err(FluentError::MessageNotFoundError(key.to_string()))
        }
    } else {
        Err(FluentError::MessageNotFoundError(key.to_string()))
    }
}

type Bundle = FluentBundle<FluentResource>;
pub fn load_locale(locale: impl AsRef<str>+Debug) -> Result<Rc<Bundle>, FluentError> {
    let locale = locale.as_ref();
    let resource = create_resource(&locale)?;
    let bundle = create_bundle(resource, locale)?;

    Ok(
        Rc::new(bundle)
    )
}

fn create_resource(locale: impl AsRef<str>+Debug) -> Result<FluentResource, FluentError> {
    let locale = locale.as_ref();
    
    let lang_folder = crate::file::get_dir_path(std::path::Path::new("lang"));
    let file = |l: &str| lang_folder.join(format!("{}.ftl", l));

    let map_err = |e| FluentError::from(e);
    let locale_file = file(locale);
    let resource = if file_exists(&locale_file) {
        get_file_text(locale_file).map_err(map_err)?
    } else {
        get_file_text(file(default_locale().as_str())).map_err(map_err)?
    };

    match FluentResource::try_new(resource) {
        Ok(resource) => Ok(resource),
        Err(_) => Err(FluentError::FluentSyntaxError)
    }
}

fn create_bundle(resource: FluentResource, locale: impl AsRef<str>+Debug) -> Result<Bundle, FluentError> {
    let locale = locale.as_ref();
    
    let langid: unic_langid::LanguageIdentifier = locale
        .parse()
        .map_err(|_| FluentError::FluentSyntaxError)?;
    let mut bundle = FluentBundle::new(vec![langid]);
    
    match bundle.add_resource(resource) {
        Ok(_) => Ok(bundle),
        Err(_) => Err(FluentError::FluentSyntaxError)
    }
}

Файлы локализации будут лежать в папочке lang рядом с исполняемым файлом программы. Для копирования папки в папку с артефактами будем использовать простой крейт copy_to_output.

cargo add --build copy_to_output
// build.rs
use std::env;
use copy_to_output::copy_to_output;

fn main() {
    copy_to_output("lang", &env::var("PROFILE").unwrap()).unwrap();
    
    slint_build::compile("components/app.slint").unwrap();
}
Файлы локализации

lang/ru-RU.ftl

app-name = Счётчик

counter-count = 
    { $count ->
        [0] Ни одного очка
        [one] {$count} очко
        [few] {$count} очка
        *[other] {$count} очков
    }

counter-add = Прибавить
counter-subtract = Убавить

lang/en-US.ftl

app-name = Counter

counter-count = 
    {$count -> 
        [0] You have no points
        [one] You have one point
        *[other] You have {$count} points
    }

counter-add = Add
counter-subtract = Subtract

Пробрасывать эти функции в DSL будем через global singleton и pure callback. Да вообще многую логику в плюс-минус крупных проектах придётся пробрасывать через global singleton, если не хотите вести огромную цепочку из свойств от своего до корневого компонента, который реализует трейт Window.

Чистый коллбэк, или pure callback – спецификатор, дающий дать понять компилятору то, что коллбэк не реактивный, то есть никакие другие свойства при его вызове не изменяются. В случае функций, компилятор сам может определить, реактивные они или нет.

Источник: slint.dev.

Для большей наглядности отлепим всю вёрстку от AppWindow, создадим файл components/counter.slint и реализуем компонент Counter.

import { VerticalBox, HorizontalBox, Button } from "std-widgets.slint";

export component Counter {
    property <int> count: 0;

    VerticalBox {
        alignment: center;
    
        Text {
            font-size: 30px;
            horizontal-alignment: center;
            text: "Count: " + count;
        }
        HorizontalBox {
            Button {
                text: "add";
                clicked => {
                    root.count += 1;
                }
            }
            Button {
                text: "subtract";
                clicked => {
                    if root.count != 0 {
                        root.count -= 1;
                    }
                }
            }
        }
    }
}

Теперь создадим файл components/globals.slint, в котором опишем синглтон Fluent с коллбэком message, который будет принимать в качестве аргумента ключ для fluent-сообщения, и коллбэк param-message, который будет так же принимать ключ и массив (а-ля модель) параметров. Параметры будут представлены в виде структур (да-да, можно даже структуры описывать).

export struct MessageParam {
    name: string,
    value: string
}

export global Fluent {
    pure callback message(string) -> string;
    pure callback param-message(string, [MessageParam]) -> string;
}

Если хотите, чтобы Fluent и структура были доступны в раст-коде, в components/app.slint (или в любой точке входа) импортировать Fluent и снова экспортировать ( ఠ ͟ʖ ఠ). Структуру MessageParam компилятор сам подтягивает, видя её в аргументах коллбэка. Наверное ¯\_(ツ)_/¯.

// components/app.slint
import { Fluent } from "globals.slint";
export { Fluent }

Для того, чтобы добавить логики для коллбэка, нужно в src/main.rs получить экземпляр Fluent и добавить ему обработчик на коллбэк через метод on_{название коллбэка}.

let app = AppWindow::new()?;

let fluent = app.global::<Fluent>();
let b = bundle.clone();
fluent.on_message(move |key| {
    match get_msg(&b, &key, None) {
        Ok(msg) => SharedString::from(msg),
        Err(_) => key
    }
});
fluent.on_param_message(move |key, args| {
    let args = FluentArgs::from_iter(
        args
        .iter()
        .map(|a| (a.name.to_string(), FluentValue::try_number(a.value.to_string())))
    );

    match get_msg(&bundle, &key, Some(&args)) {
        Ok(msg) => SharedString::from(msg),
        Err(_) => key
    }
});

app.run()?;

Теперь можем убрать свойство window_title и код установки значения этого свойства в компоненте AppWindow и просто воспользоваться нашим модулем Fluent. Точно так же и с компонентом Counter.

// components/counter.slint
import { VerticalBox, HorizontalBox, Button } from "std-widgets.slint";
import { Fluent } from "globals.slint";

export component Counter {
    property <int> count: 0;
    pure function get-counter-fluent() -> string {
        Fluent.param-message("counter-count", [{name: "count", value: count}] )
    }
    function update-counter() {
        counter.text = get-counter-fluent();
    }

    VerticalBox {
        alignment: center;
    
        counter := Text {
            font-size: 30px;
            horizontal-alignment: center;
            text: get-counter-fluent();
        }
        HorizontalBox {
            Button {
                text: Fluent.message("counter-add");
                clicked => {
                    root.count += 1;
                    update-counter();
                }
            }
            Button {
                text: Fluent.message("counter-subtract");
                clicked => {
                    if root.count != 0 {
                        root.count -= 1;
                        update-counter();
                    }
                }
            }
        }
    }
}
Люблю русский язык, хотя бы за то, что в нем есть гениальная фраза «да нет наверное»
Люблю русский язык, хотя бы за то, что в нем есть гениальная фраза «да нет наверное»

Иконка программы

Прекрасно, но не прекрасно далёко. А именно у нас нет нашей брендовой иконки! Предлагаю это исправить. Ищем нашу прекрасную иконку в png и ico форматах и закидываем в папку icons. В файле components/app.slint добавляем свойство icon и с помощью директивы @image-url указываем путь до нашего файла относительно компонента.

export component AppWindow inherits Window {
    icon: @image-url("../icons/icon.png");
    // <...>
}

В файле Cargo.toml нужно добавить в зависимости winres. Крейт генерирует файлы ресурсов, поэтому иконка файла будет работать только под Windows.

[target.'cfg(windows)'.build-dependencies]
winres = "0.1.12"

Теперь в build.rs добавим следующий код:

if cfg!(target_os = "windows") {
    winres::WindowsResource::new()
        .set_icon("icons/icon.ico")
        .compile()
        .unwrap();
}

Калькулятор

Предлагаю чуть усложнить задачу вёрстки и закончить всё-таки наш калькулятор. Хотелось бы сделать полноценный интерпретатор математических выражений на комбинаторах, но думаю, что этот тема для отдельной статьи.

Создадим файл components/calculator.rs с компонентом Calculator (тем временем наверное, читатель: (╯°□°)╯︵ ┻━┻). В котором определим свойство, в котором будет массив массивов со значениями кнопок.

import { Button, VerticalBox, HorizontalBox } from "std-widgets.slint";

export component Calculator {
    property <[[string]]> buttons: [
        ["1", "2", "3", "+"],
        ["4", "5", "6", "-"],
        ["7", "8", "9", "*"],
        ["C", "0", "=", "/"]
    ];
    
    VerticalBox {
        for row in root.buttons: HorizontalBox {
            for button in row: Button {
                text: button;
            }
        }
    }
}

Прекрасно. А теперь добавим поля, куда будут выводиться результаты ввода-вывода.

VerticalBox {
    VerticalBox {
        Text {
            text: "0";
            font-size: 30px;
            opacity: 0;
            horizontal-alignment: right;
            vertical-alignment: center;
        }
        Text {
            text: "0";
            font-size: 60px;
            horizontal-alignment: right;
            vertical-alignment: center;
        }
    }

    for row in root.buttons: HorizontalBox {
        for button in row: Button {
            text: button;
        }
    }
}

Предлагаю задать состояние компоненту, которое будет хранить такое, как текущее значение, прошлое значение, оператор и флаг, устанавливаемый после вычисления значения.

struct CalcState {
    current-value: int,
    last-value: int,
    operator: string,
    computed: bool
}

export component Calculator {
    property <CalcState> state: { current-value: 0, last-value: 0, operator: "", computed: true };
    //<...>
}

Флаг computed сразу установлен в true, чтобы верхнее поле было прозрачным до начала ввода.

Можно было бы реализовать модуль MathLogic, или что-то типа того, чтобы придерживаться принципа разделения бизнес-логики и представлений, но это чрезмерно в нашем случае и будем всё реализовывать силами Slint DSL, который в конечном итоге всё равно будет скомпилирован в нативный код.

export component Calculator {
    function get-state() -> CalcState{ { current-value: 0, last-value: 0, operator: "", computed: true } }
    property <CalcState> state: get-state();
    property <[[string]]> buttons: [/*<..>*/];
    
    callback value-computed;
    callback state-cleared;
    callback operator-pressed(string);
    callback number-pressed(int);
    
    // Не кидайте в меня тапки, но да, обработка действий в коллбэках
    operator-pressed(operator) => {
        if (state.computed) {
            state.last-value = state.current-value;
            state.current-value = 0;
            state.computed = false;
        } else if (state.operator == "") {
            state.last-value = state.current-value;
            state.current-value = 0;
        }
        state.operator = operator;
    }
    state-cleared => {
        state = get-state();
    }
    number-pressed(number) => {
        if (state.computed) {
            state.last-value = 0;
            state.operator = "";
        }

        state.current-value = state.current-value * 10 + number;
    }
    value-computed() => {
        state.computed = true;

        if (state.operator == "+") {
            state.current-value = state.last-value + state.current-value;
        } else if (state.operator == "-") {
            state.current-value = state.last-value - state.current-value;
        } else if (state.operator == "×") {
            state.current-value = state.last-value * state.current-value;
        } else if (state.current-value != 0) {
            state.current-value = state.last-value / state.current-value;
        }
        
        state.last-value = 0;
        state.operator = "";
    }
    // Роутинг кнопок
    function route-actions(action: string) {
        if action == "=" {
            value-computed();
        } else {
            state-cleared();
        }
    }
    function button-pressed(button: string) {
        if (is-operator(button)) {
            operator-pressed(button);
        } else if (is-action(button)) {
            route-actions(button);
        } else {
            number-pressed(button.to-float());
        }
    }
    
    //<...>
    
    pure function is-operator(button: string) -> bool {
        button == "+" 
        || button == "-" 
        || button == "*" 
        || button == "/"
    }
    pure function is-action(button: string) -> bool {
        button == "=" || button == "C"
    }
}

Теперь немного поменяем шаблон, добавив вывод значений и приправив всё это капелькой анимаций.

export component Calculator {
    function get-state() -> CalcState{ { current-value: 0, last-value: 0, operator: "", computed: true } }
    property <CalcState> state: get-state();

    //<...>

    VerticalBox {
        VerticalBox {
            Text {
                text: "\{state.last-value} \{state.operator}";
                font-size: 30px;
                opacity: 0;
                horizontal-alignment: right;
                vertical-alignment: center;

                states [
                    visible when !state.computed : {
                        opacity: 1;
                        in {
                            animate opacity { duration: 120ms; }
                        }
                        out {
                            animate opacity { duration: 60ms; }
                        }
                    }
                ]
            }
            Text {
                text: state.current-value;
                font-size: 60px;
                horizontal-alignment: right;
                vertical-alignment: center;
            }
        }

        for row in root.buttons: HorizontalBox {
            for button in row: btn := Button {
                text: button;
                clicked => { button-pressed(btn.text) }
            }
        }
    }

    //<...>
}

На последок можно сделать для стиля полупрозрачный фон окна с помощью функции Colors.rgba и свойства background . Вообще с этим свойством рекомендую быть осторожными, ибо оно переопределяет прозрачность у всего, что отрисовывается в окне (●'◡'●). Есть ещё свойство opacity, но оно не работает на окне, поэтому имеем то, что имеем.

import { Calculator } from "calculator.slint";

export component AppWindow inherits Window {
    in property <string> window_title;

    width: 300px;
    height: 500px;
    title: window_title;
    icon: @image-url("../icons/icon.png");
    background: Colors.rgba(28,28,28,0.9); // <-- Делаем окно чуть-чуть прозрачным
    default-font-size: 20px;
    
    Calculator {
        width: parent.width;
        height: parent.height;
    }
}

Кстати, если я ещё не говорил, то когда указываете свойства width и height у окна, то оно перестаёт быть resizable. Если хотите сохранить возможность спокойно растягивать окошко, нужно использовать либо min-,max-, либо preferred- свойства. Первый ограничивают размеры, на которые можно растянуть окно, а второе без каких-либо ограничений.

В случае обычных компонентов, а не окн, то все они работают похожим образом. Стандартные width-height задают жёсткие размеры, делая компоненты неотзывчивыми, статическими. min-, max- свойства задают границы, по которым может растягиваться компонент, а вот preferred- свойство задаёт предпочитаемые размеры, но не гарантирует, что они будут. То есть, условно говоря, компоненту задали preferred-width: 10000px, а ширина родителя всего 200px, поэтому компонент будет принудительно уменьшен в эти пределы.

Паскаль может гордиться нами
Паскаль может гордиться нами

Вездесущий веб

Я как-то даже и не задумывался о том, чтобы портировать калькулятор в веб, но @stanukih задал на этот счёт вопрос, поэтому прошу вашему вниманию... *барабанная дробь*... калькулятор в браузере!

Изменения по коду будут минимальными, а компоненты трогать вообще не будем, чтобы посмотреть на то, сколько сил придётся приложить к портированию программы в веб. Весь код будет в соответствующей ветке.

Для того, чтобы скомпилировать нашу программу в пригодный для использования в вебе вид, нужен тулчейн wasm-pack. Но у нас не получится скомпилировать наш код в WASM-модуль, так как тип нашего проекта bin. Идём в файл Cargo.toml и приводим его в следующий вид:

[package]
name = "calc-rs"
version = "0.1.0"
edition = "2021"

[lib]
path = "src/main.rs" # Можно не указывать, если переименовать файл src/main.rs в src/lib.rs
crate-type = ["cdylib"]

[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = { version = "0.2" }
web-sys = { version = "0.3", features=["console"] }

[dependencies]
slint = "1.5.1"

[build-dependencies]
slint-build = "1.5.1"

Прекрасно. Теперь шагаем в наш src/main.rs, отрекаемся от стандартной библиотеки, подавляем жалобы линтера, импортируем прелюдию wasm_bindgen и указываем ему точку входа.

#![no_std]
#![warn(dead_code)]

#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;

use slint::SharedString;

slint::include_modules!();

#[cfg_attr(target_arch = "wasm32", wasm_bindgen(start))]
fn main() {
    let app = AppWindow::new().unwrap();
    app.set_window_title(SharedString::from("Calculator"));
    app.run().unwrap();
}

Пришло время скомпилировать проект. Для этого запускаем ранее установленный сборщик:

wasm-pack build --release --target web --no-default-features

Эта команда автоматом скомпилирует наш Rust-код в WASM-модуль и ко всему этому сгенерирует за нас JS-обёртку и файл с описанием типов Typescript. Разве не сказка?

Артефакты после wasm-pack
Артефакты после wasm-pack

Создаём index.html с довольно простым содержимым. Slint будет искать canvas с идентификатором... «canvas».

<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <title>Calculator</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        html,
        body {
            height: 100%;
            width: 100%;
        }

        body {
            display: flex;
            justify-content: center;
            align-items: center;
        }
    </style>
  </head>
  <div id="spinner" style="position: relative;">
    <div class="spinner">Loading...</div>
  </div>
  <canvas id="canvas" unselectable="on"></canvas>
  <body>
    <script type="module">
        import init from "./pkg/calc_rs.js";
        init().finally(() => {
            document.getElementById("spinner").remove();
        });
    </script>
  </body>
</html>

Можете попробовать запустить просто html-файлик, но у вас будет, скорее всего, ругаться на нарушение CORS-политики. Поэтому запустим небольшой локальный сервер. Лично я буду пользоваться http.server из стандартного тулчейна Python.

python -m http.server
Может, Паскаль и похвалит, а вот Марк Уиллер достал топор...
Может, Паскаль и похвалит, а вот Марк Уиллер достал топор...

Я так и не понял, как заставить его растягиваться, так как автоматом канвасу выставляются те же размеры, что указаны в width и height. При этом, если выставить в процентах, эти параметры игнорируются и принимаются минимально возможные. Единственный вариант, который я вижу, так это делать бинды к JS, подписываться на события изменения размера окна и прокидывать новые значения через свойства компонентов.

Возможно, не я один заметил, что вся вёрстка переняла пьяный стиль старика Соу? Скорее всего, это из-за того, что вместо GridLayout были использованы VerticalLayout и HorizontalLayout, так как первый не поддерживает циклы.

Почему игнорируются цвета шрифта и кнопок – самая настоящая тайна для меня. Либо это какие-то ограничения WebGL, либо темы из стандартной библиотеки не подгружаются по каким-то причинам.

Про часть документации, где описывается компиляция в WASM и дальнейшая работа, я вообще молчу, её, считай, и нет. Поэтому можно считать, что Slint, надеюсь, пока что совершенно не подходит для разработки веб-страниц. Более интересно для веба выглядит какой-нибудь Yew или вообще фуллстек крейт Dioxus.

Итого

Был пройден путь от счётчика до кривого калькулятора, в ходе которого были рассмотрены почти все возможности библиотеки Slint, хоть и в сжатом формате (привет, состояния и переходы). Под почти все я имею ввиду и i18n, и более сложные сценарии. Например, запрос к API при инициализации компонента или вызов асинхронных функций.

Запросы разработчиков целевой платформы библиотека покроет, но писать более сложные интерфейсы, скорее всего, будет довольно сложно и нецелесообразно. Например, в примерах я не видел ни одного, где был бы элементарный drag`n`drop. И в документации не нашёл ни одного упоминания прозрачности окна, до чего я дошёл сам. А GridLayout до сих пор сырой и вы не сможете его использовать вместе с циклами.

Но судя по репозиторию разработчиков, разработка идёт активным ходом. Хотя стоит учитывать, что они поддерживают свой DSL и три ЯП, поэтому возникают вопросы на счёт целесообразности использования DSL и поддержки трёх ЯП, особенно Node.js, так как это, скорее всего, замедляет развитие проекта (обожаю тавтологии). Могли бы ограничиться либой на тех же плюсах, а уж бинды сообщество само бы смогло сделать.

На основе всего вышесказанного могу порекомендовать использовать другую библиотеку, если ваша цель не написать красивый интерфейс для принтера или кофемашины, например, Tauri, Avalonia или Flutter.

Ссылочки

Репозиторий с кодом: https://gitverse.ru/ertanic/calc-rs.

Репозиторий Slint: https://github.com/slint-ui/slint.

Комментарии (80)


  1. orekh
    10.04.2024 03:35
    +3

    Опять либа чуть менее чем полностью состоящая из макросни. Автору статьи респект, что не стал скрывать проблемы!


    1. domix32
      10.04.2024 03:35
      +1

      Почему это проблема?


      1. DarkEld3r
        10.04.2024 03:35

        Yвеличение времени компиляции и IDE с макросами не очень хорошо работают. Хотя в данном случае мне кажется, что от этого никуда не деться.


        1. domix32
          10.04.2024 03:35

          Ну, тут действительно стоит выбор или писать больше кода, но без макросов, либо писать меньше кода, но дольше компилировать. За поддержку со стороны IDE ничего не могу сказать, ибо для раста пользуюсь по большому счёту только подсветкой и подстановкой из скоупа файлов. Для остального у меня есть манулы мануалы.


  1. SpiderEkb
    10.04.2024 03:35

    Я правильно понимаю, что весь интерфейс прописывается ручками в виде

    export component Calculator {
        property <[[string]]> buttons: [
            ["1", "2", "3", "+"],
            ["4", "5", "6", "-"],
            ["7", "8", "9", "*"],
            ["C", "0", "=", "/"]
        ];
        
        VerticalBox {
            for row in root.buttons: HorizontalBox {
                for button in row: Button {
                    text: button;
                }
            }
        }
    }

    ?

    Извините, на дворе какой год? Интерактивное рисование форм появилось еще в 90-х годах прошлого столетия. В простейшем виде - MSVC 1.52 под Win 3.11 все это уже было. Дальше - Delphi и C++ Builder - там вообще все было мощно.

    А вот такое вот я в последний раз помню для какой-то библиотеки, реализующей текстовый интерфейс с менюшками и прочим под DOS... Это где-то конец 80-х годов прошлого века.

    И уж если мы говорим про винду, то там для всего этого есть специальные .rc файлы, где хранится описание всего интерфейса и на работу с которыми заточен WinAPI.

    Ну, или, коль хочется мультиплатформенности, то есть же wxWidgets тот же. Где есть wxCrafter, wxFormBuilder где рисуешь форму, а оно само генерит весь этот код.

    Писать руками мало-мальски сложные интерфейсы таким вот образом - 80% времени на это уйдет. Еще и, подишто, на бумажке придется все это рисовать, если у вас на форме пара десятков контролов (менюшки, списки, выпадающие списки, движки, счетчики, текстовые формы с валидацией ввода, футбары с иконками, статусбыры в какими-то контролами и т.п.)

    Как-то несерьезно все это...

    Быстрый гуглинг дал пару ссылок:

    https://crates.io/crates/wx-rs
    https://rust.libhunt.com/wxrust-alternatives

    Не смотрели в эту строну?


    1. InoyChel
      10.04.2024 03:35
      +9

      Расскажите это всему frontend сообществу, где есть куча фреймворков которые описывают интерфейсы через html/xml, css и js.

      wxWidgets - ужасен, мне довелось с ним поработать из под python. Он тормознутый, не может даже 2 десятка мелких картинок отобразить, так еще и кривой до кучи. Да банально - он морально устарел. Сколько было хаков в проекте для того что бы сделать тривиальные вещи. Так к тому же все было описано кодом. Билдер форм пытались использовать для интерфейсов определенных пользователем, но он просто глючил и вылетал.

      WinApi - только под винду, сейчас правят браузеры и смартфоны.

      Насколько я слышал даже разработчики на Qt предпочитают QML.

      Несерьезно это застрять в каком-то своем мире и не видеть развития вокруг. Мир давно уже ушел вперед, а вы все так же смотрите на свою зеленую траву. Попробуйте научиться чему-то новому, оно придумано не от хорошей жизни.


      1. SpiderEkb
        10.04.2024 03:35
        +4

        Я в свое время написал немало интерфейсов в самых разных средах.

        И сейчас иногда приходится делать (правда, это вообще что-то с чем-то - AS/400, текстовые экранные и принтерные формы, описываемые на DDS - data defenition specifications, но там тоже есть редактор визуальный).

        И в целом представлю себе неплохо затраты времени на ручное написание кода формы и возможность ее визуального проектирования когда ты сразу видишь как она будет выглядеть "в натуре". Что там будет генериться на выходе - код, QML, DDS - абсолютно не важно. Важно что вы сразу видите результат. И можете двигать, добавлять, удалять контролы, редактировать их свойства здесь же. По времени и усилиям несопоставимо. В визуальном редакторе все это в разы быстрее делается. Потом только логику прописываешь в готовый уже код.

        Достаточно прост о сделать что-то более-менее объемное - хотя бы десяток форм с 5-15 контролов на каждой чтобы понять что рисовать их в специальном редакторе, где сразу видишь как оно будет выглядеть, намного эффективнее. Особенно, когда надо добавить новый контрол туда, где уже есть еще 15 и их надо подвигать туда-сюда...

        Ваш калькулятор в делфи или билдере делается за 15-20 минут левой ногой. И руками там потребуется написать десяток-другой строк кода, не больше.

        сейчас правят браузеры и смартфоны

        То, что в статье описано - оно под браузер или под смартфон? Или таки под десктоп?

        В винде все сделано достаточно разумно, кстати. Есть rc файл с описанием формы и есть движок, который эту форму отрисовывает по данным из rc. А в коде вы только получаете сообщения о том, что там делает пользователь и обрабатываете их.

        Сделайте для RUST что-то подобное - будет удобно и красиво. Описание форм может быть любым - QML, DDS - что угодно. Важно что для описания интерфейсам вам не придется набивать десятки станиц кода и при этом держать в голове всю разметку.

        Несерьезно это застрять в каком-то своем мире и не видеть развития вокруг. Мир давно уже ушел вперед

        "Вперед" - это куда? К тому, как это делалось под DOS в 80-х годах? Я все это еще застал. И даже тогда уже были попытки к описанию интерфейсов не в коде, а специальными скриптами. А код уже или генрирился по ним, или использовал их как данные для отрисовки.

        А для любителей задолбаться могу предложить писать документы не в ворде, а в ноутпаде в формате postscript.


        1. SpiderEkb
          10.04.2024 03:35
          +3

          Просто как пример (далеко не самое сложное что приходилось делать, просто это близко лежало)

          Сколько времени уйдет на описание кодом таких вот форм?

          А чего-то вот такого:

          Со всеми двигающимися сплиттерами, выравниванием и т.п.

          Вот просто чтобы все это на экран вывести. Даже без логики заполнения данными, обработки действия пользователя (отметка элемента в таблице, перетаскивания мышкой карты, изменения размеров элементов сплиттерами и т.п.)


          1. mayorovp
            10.04.2024 03:35
            +4

            Вот вторую половину второй формы гораздо проще описать кодом чем натыкивать вручную. А потом ещё и использование этих данных в коде заодно упростится.

            Да и с первую я бы попытался кодом сделать.

            И да, в нижнем окне вы лукавите, потому что там видны стандартные компоненте, который явно вручную не рисовались (собственно, если точный дизайн не важен, именно их я бы и на вторую форму засунул). Да и двигающиеся сплиттеры с выравниванием - это именно то, что в коде делать проще.

            Вот третья форма - да, явная демонстрация достоинств визуального создания интерфейсов.


            1. SpiderEkb
              10.04.2024 03:35
              +1

              При "натыкивании вручную" можно точно также копипастить, как в коде - нормальный редактор позволяет дублировать однотипные элементы, использовать выравнивание относительно самой формы, других элементов.

              Т.е. выделил группу элементов - "это привязываем к верхнему краю", для другой группы - "это в левому краю" и т.п.

              Все свойства контролов тоже там же описываются, включая сообщения, которые нужно посылать в форму при тех или иных действиях. В Дельфи/Билдере просто "добавить форму", потом рисуешь ее и на выходе имеешь код-болванку где уже все обработчики сообщений обозначены, остается только свою логику прописать.

              Просто количество нажатий на клавиши сокращается в разы. И размер кода (за счет того, что все это рендерится на кодом, а самой виндой) сильно меньше (а значит и работать с ним проще).

              Тут сам принцип работы - есть описание интерфейса, есть рендер (на уровне системы или конечного устройства). В коде вы только обрабатываете события от рендера.

              Все это проявляется особенно когда у вас достаточно плотная форма, а вам говорят "нужно еще два поля для ввода добавить". И начинается - "это поле чуть сократим, это сдвинем вниз и влево, это вверх и вправо, вот местечко разгребли и сюда воткнем новое поле...". Делать это мышкой на экране намного проще и удобнее, чем в тексте, пытаясь представить а как оно все в итоге будет выглядеть - не наедут ли поля друг на друга..."

              А если у вас еще все это разбито на группы разделенные сплиттерами и в группах выравнивание относительно друг друга, то имеется режим предпросмотра, то сразу можете смотреть не разъезжается ли что-то (или не наезжает друг на друга) если сплиттеры начинаете двигать. И для этого не надо ничего компилировать и запускать.

              А если все это еще потом хранится в виде QML или DDS, то можно руками сделать тонкую доводку в тексте. Но 80-90% рутинной работы вы быстро сделаете визуально.


              1. mayorovp
                10.04.2024 03:35
                +2

                При "натыкивании вручную" можно точно также копипастить, как в коде

                В визуальном редакторе копипастить сложнее, как бы это странно не звучало. После каждого копипаста надо переключаться в окно свойств элемента и там менять значения, выискивая их в огромном списке.

                В коде же настройки, имеющие значения по умолчанию, как правило места не занимают. Кроме того, в коде зачастую и копипастить не нужно, ибо есть циклы и подпрограммы.

                В Дельфи/Билдере просто "добавить форму", потом рисуешь ее и на выходе имеешь код-болванку где уже все обработчики сообщений обозначены, остается только свою логику прописать.

                Не все, а те которые вы сами попросили добавить. Та же Delphi даже автоматически убирала пустые обработчики. Потому что всевозможных обработчиков попросту слишком много.

                Все это проявляется особенно когда у вас достаточно плотная форма, а вам говорят "нужно еще два поля для ввода добавить". И начинается - "это поле чуть сократим, это сдвинем вниз и влево, это вверх и вправо, вот местечко разгребли и сюда воткнем новое поле..."

                На плотной форме - да, а вот на обычной это работает в другую сторону. Там, где в декларативной разметке можно просто добавить строчку и остальные сдвинутся вниз - в классическом визуальном редакторе надо все элементы сдвигать вниз вручную. И пытаться не добавить случайно им обработчик в процессе сдвигания.

                PS а помните ли вы слишком большие формы, которые приходилось редактировать с прокруткой?


              1. Pardych
                10.04.2024 03:35
                +1

                Вы просто пропустили лет 15 эволюции гуев и абсолютно не знакомы ни с современными фреймворками (на которых реально проще накидать текстом со всеми современными ide), ни с современными требованиями к этим самым гуям. Говорю как человек много и с удовольствием красящий кнопки в очень разных стеках с 2002 года. Стало лучше, стало веселей. Текст даже не пишется, а собирается как конструктор. Интерфейс же теперь - не статичные формы с обработчиками, а динамичная структура, которую проще и нагляднее описывать в немногословных декларативных фреймворках. Уж поверьте на слово тому кто встретил столкновение винформз и первых промышленных планшетов на винде, что вызвало необходимости изобретать dip чтобы пользователь попадал в кнопку своим мозолистым толстым пальцем. И одновременно свело на нет всю полезность визуального редактора.

                Мир интерфейсов куда интереснее и многообразнее задачи накидать кучу элементов на статичную форму. И фреймворки эволюционируют в код-фест не потому что чьей-то левой пятке так захотелось, а потому что эффективность подхода была проверена.


                1. HemulGM
                  10.04.2024 03:35

                  И вы тоже заблуждаетесь по части визуальных редакторов. Скорее всего по тому что используете в основном фреймворки без них.

                  Интерфейс же теперь - не статичные формы с обработчиками, а динамичная структура

                  Само собой, что не статичная и визуальный редактор не является ограничителем или причиной статического интерфейса.

                  Любой интерфейс не состоит из случайных контролов, он состоит из смысловых блоков: блок элементов списка, блок с меню, блок с набором полей, блок с рендером. Форма состоит из этих блоков, которые можно описать отдельно.

                  Я могу в дизайнере отдельно построить интерфейс конкретного блока и использовать его в разных формах. В WinForms это либо не используется, либо вообще не доступно (я уже не помню). Речь идет о фреймах и схожего на них функционала.

                  Пришло ТЗ на программу с ссылкой на фигму. Фигма тоже раскидывает всё на блоки. Вижу конкретный вид элемента списка, создаю стиль такого элемента (визуально). Теперь в любой список я могу добавить элемент с таким стилем. Стиль кнопки - сделано, стиль элемента меню - сделано, стиль самого меню - сделано. Для всего этого мне не нужно написать ни строчки кода.

                  Мир интерфейсов куда интереснее и многообразнее, чем вам кажется.

                  Hidden text
                  Стиль элемента списка - карточка
                  Стиль элемента списка - карточка
                  Стиль элемента списка - строка с картинкой
                  Стиль элемента списка - строка с картинкой
                  Фрейм блока сообщения
                  Фрейм блока сообщения
                  Фрейм отображения кода в блоке сообщения
                  Фрейм отображения кода в блоке сообщения

                  Блок меню, блок чата, блок сообщения, блок конкретных данных в сообщении
                  Блок меню, блок чата, блок сообщения, блок конкретных данных в сообщении


                  1. Pardych
                    10.04.2024 03:35

                    Приведенный вами пример на любом современном фреймворке пишется буквально в несколько строк кода. А еще позволяет без напильника подставлять любое количество вариаций одного и того же элемента в зависимости от условий - хошь по параметрам отображения, хошь состояния. И разносить их по разным экранам если они вдруг маленькие телефонные. И при переходе между экранами красиво превращать один элемент в другой. Стилизация с той же фигмы переносится практически в лоб (и это мы веб не берем где она вообще генерится какая нужна при правильном дизайнере и переносится 1 в 1).

                    Ну то есть, блин, дельфи как иллюстрация моих слов про разнообразие? Серьезно? Я там уже писал что уход от визуалщины не левой пяткой разработчиков мотивирован. Как у дельфей с версткой в макетных единицах, например?


                    1. HemulGM
                      10.04.2024 03:35

                      Я ещё раз убеждаюсь, в том что вы мало что знаете о современно Делфи.

                      Стиль из скринов сверху может быть применен к любому элементу управления и изменен в любое время. А поведение элементов зависит от внешнего контейнера (хошь разноси, хошь не разноси).

                      Уход от визуальщины мотивирован отсутствием нормальных инструментов, а существование Фигмы тому прекрасное подтверждение.

                      У Делфи с единицами размеров и позиционирования всё прекрасно. Прекрасно всё скейлится автоматически и рисуется замечательно на разных экранах. Об этом я даже не забочусь при создании GUI.

                      Прежде чем минусить, изучите вопрос. Ознакомьтесь с тем, что и как работает и какие доступны возможности.

                      Hidden text


                      1. Pardych
                        10.04.2024 03:35

                        Это не я. Вообще не имею привычки минусы ставить.

                        Макетные единицы я не просто так взял. Это священный грааль всех дизайнеров за который они бьются с разрабами, так же известный как пиксель перфект. И от которого разрабы всеми силами отбиваются. На текущий момент я не видел их внятной имплементации ни в одном фв вообще кроме флаттер/композ/свифтюи - то есть декларативных. Вот пример: https://pub.dev/packages/flutter_screenutil

                        Я не говорю что ваш современный дельфи плох и да я действительно его не знаю. Мой спич о том что при переходе в код с визуала мы в эффективности не теряем, а приобретаем, потому как получаем естественным образом доступ к тому что в любых визуальных разделено на этапы - и верстку и биндинг и стейт-менеджмент и прочее любое. И это я говорю на основе собственного опыта перехода с нативных вьюшек дроида на композ например (хотя есчестно нативные вьюшки тоже проще было руками в xml верстать и весь редактор там был нужен только как превью, несмотря на то что в нем и была вся полнота возможностей для верстки).

                        Когда мы смотрим на код мы ясно и однозначно видим что происходит, а WYSWYG, грубо говоря, это не совсем правда. При достаточно вычурных требованиях у нас нет соответствия того что мы видим тому что получаем. Если вы не сталкивались с этим достаточно часто, то это всего лишь недостаток опыта. Оно как раз чаще всего именно так.


                      1. HemulGM
                        10.04.2024 03:35

                        Единственное отличие визуального создания и декларативного - это то, что я вижу сразу как оно будет и должно выглядеть. В остальном никаких ограничений нет. Визуальный редактор лишь позволяет и помогает строить код интерфейса. Результат его работы в любом случае - код. В случае Делфи не конечный код, а свой формат, но это по сути ничего не меняет. Я также могу переключиться на код и добавить части интерфейса через него. А могу использовать визуальный редактор и то же самое сделать, но сразу с возможностью увидеть.

                        Весь спор сейчас состоит в том, что я знаю возможности визуального редактора в Делфи, а вы, к сожалению, нет. И я пока не встречал того, чего бы я не мог сделать через него. На моём счету много проектов и часть из них шла с проектом в Фигме. Реализовал я абсолютно всё, что требовалось, включая анимации, эффекты, переходы и т.д. Визуальный редактор в Делфи далеко ушел от WinForms, особенно в кроссплатформенном фреймворке, о котором я говорю.


                      1. Pardych
                        10.04.2024 03:35

                        Весь спор сейчас состоит в том что вы почему-то считаете что я спорю про дельфи. Я дал вам одну-единственную штуку которой в вашем дельфи нет и быть не может, в остальном я его никак не третирую, а говорю про разный подход и почему концепция декларативок не менее, а чаще более хороша, а фв не предлагающие редактора могут быть лучше во многих моментах. А вы мне дельфи то, дельфи се. Вы вообще с ним зачем-то решили подколоть меня насчет моей фразы о разнообразии мира юай. Хотя писал я и про веб (с его там всякими реактами и вью жс) и про свинг (на котором до сих пор почти все продукты JB) и про джава эфикс (с его полумертвым, но неплохим торнадо) и про нативную дроид-верстку (с живым редактором в котором тоже при желании все можно), и про кутэ (господи никогда больше) и про винформс (в разрезе его давнего столкновения с новой реальностью) и про композ/флаттер/свифтюи как однояйцевых близнецов ставших мейнстримом не за красивые редакторы. Ну как-то так. И тут вы "а я вот могу все это на дельфи, смотрите ". Я могу все это же на всем вышеперечисленном. Причем шустрее и приятнее всего на последних трех. Вот про это "шустрее и приятнее" речь и шла.


                      1. HemulGM
                        10.04.2024 03:35

                        Так о том и речь, "я могу, смотрите" - демонстрация того, что вы не знаете на что способы визуальные редакторы. И о какой "штуке", которой нет в Делфи, идет речь?

                        Вы ограничены в визуальных редакторах своим опытом.


                      1. Pardych
                        10.04.2024 03:35

                        Интересная концепция "если вы можете что-то на дельфи, то значит я этого не знаю про визуальные редакторы". Это в какой логике? Я вам абсолютно точно не писал того что вы не можете собрать насчастный экранчик на дважды два линейных лейаута с двумя списочными элементами и горстью контролов попроще.

                        И да, мы все ограничены своим опытом. Но бога ради прекратите ограничивать себя своими фантазиями в дискуссии с живыми людьми. Нужно ж как-то услышать что ли.


                      1. HemulGM
                        10.04.2024 03:35
                        +1

                        Перевирание слов - это "интересная концепция". Я сказал, что визуальный редактор визуальному редактору - рознь. И сказал, что не согласен с вашей точкой зрения, потому что работаю с таким визуальным редактором, который меня ничем не ограничивает. Вы с ним не работали, но уверены, что он не справляется с тем, с чем вы справляетесь декларативным подходом. Всё так? Или я что-то упустил? Пример который я показал - лишь небольшой пример и небольшого проекта, который я могу показать. А за "двумя лейаутами" скрывается масса функционала и адаптивности, которую вы не видите.

                        Подходов в визуальном редакторе может быть много и не стоит голословно заявлять, что ни один такой редактор не позволяет то, что позволяет декларативный подход.

                        То, что ты пишешь декларативно я в точности повторяю визуально. Речь не о конечном результате, а о процессе разработки. Ты пишешь начало блока слоя, я просто его туда помещаю, ты пишешь код настроек свойств, я их указываю визуально (и да, мне не нужно лезть в документацию, чтобы их узнать).

                        Редактор Делфи далеко ушел от "открыл форму, кинул кнопку и поле". Да это просто и быстро, но это не гибко и приводит к статичным интерфейсам, как ты сам и говорил. В Делфи же подход шаблонов-стилей, фреймов-заготовок. Да, ты можешь так же открыть форму и кинуть кнопку, только помимо этого ты можешь придать любой вид любому контролу в любой момент времени. Любой контрол может быть помещен в любой контрол, что позволяет делать шаблон. Шаблон-стиль - это набор других контролов, которые тоже могут иметь свой стиль. Шаблон-стиль является представлением контрола (каким бы он ни был). Разные слои предоставляют разное поведение дочерних контролов. Компоненты анимации, которые могут быть добавлены в шаблон обеспечивают интерактивность.

                        Ты пытаешься мне доказать, что декларативный подход лучше, не аргументируя это абсолютно ничем, чего не позволяет редактор в котором я работаю. Проблема в том, что ты убежден, что что-то точно можно сделать только через декларативны подход, я же, не понимаю о чем именно ты говоришь, потому что я не сталкиваюсь в редакторе с этим. И не надо говорить, что "ты не сталкиваешься, потому что что-то не делал". Я делал много и разные интерфейсы, повторяю, опыта работы в этом редакторе у меня много. И пока ты не начнешь предметно говорить, этот диалог ни к чему не приведет.

                        Если знаешь, что сделать через декларативный подход можно, а через визуальный редактор - нет, говори. А я скажу как это делается в том редакторе или соглашусь, что этого сделать нельзя.


                      1. Pardych
                        10.04.2024 03:35

                        Я сказал, что визуальный редактор визуальному редактору - рознь. И сказал, что не согласен с вашей точкой зрения, потому что работаю с таким визуальным редактором, который меня ничем не ограничивает. Вы с ним не работали, но уверены, что он не справляется с тем, с чем вы справляетесь декларативным подходом. Всё так?

                        Я дальше не читал, извините. Вообще не так. И последние реплики три я пытаюсь об этом вам сообщить. Вы ведете дискуссию не со мной, так что дальше и сами справитесь.


          1. domix32
            10.04.2024 03:35
            +4

            То есть вся претезия к фремворку - отсутствие WYSWYG редактирования. Это как минимум странно. Ну и во вторых все вот эти ваши тык тык формы что в делфи, что в студии работали только под винду. Если появился Линукс - извольте обмазываться QT/Gtk, которые занимаются ахтунгом похлеще чем у автора. А уж если приложение внезапно захотели в веб или мобилки, то здраствуйте я ваша тётя - тут даже наличие Qt Creator + QML не спасёт вас от допиливания напильником. Это не говоря уже про адок, который спрятан за теми библиотеками компонентов, который во многих случая проще взять и переписать. Только компонент в библиотеке не появится, чтобы его wyswygать.


          1. Layan
            10.04.2024 03:35

            Без логики заполнения и работы такие формы нелюбимые вами фронтендеры сделают кодом менее чем за час. И работать это будет не только в Windows.


          1. Drucocu
            10.04.2024 03:35
            +4

            Многие из нас делали такие интерфейсы... на курсовых в университете.

            С тех пор появилась целое направление, называемое UX/UI-дизайн. И если вам хочется возразить, что это бесполезные люди и их работа никому не нужна, то просто в очередной раз продемонстрируете свой весьма ограниченный взгляд на мир.


        1. diakin
          10.04.2024 03:35

          "Вперед" - это куда?

          Мусорная куча растет все выше, и старые слои скрываются под новыми завалами.


    1. diakin
      10.04.2024 03:35

      Это же формошлепство!


      1. HemulGM
        10.04.2024 03:35

        И?


        1. diakin
          10.04.2024 03:35

          /s


    1. Zuy
      10.04.2024 03:35

      похоже наигралась индустрия с интерактивным дизайном. Даже Андроид переходит на декларативный подход с их Compose.


      1. SpiderEkb
        10.04.2024 03:35

        На самом деле ничего хорошего в этом нет. И не "наигрались", а нет нормального мультиплатформенного рендера интерфейсов по их описанию (в том же QML), который работа бы везде. Вот и откатились на уровень DOS 80-х годов где все это ровно также делалось.

        В результате приходится держать отдельных "фронтенд разработчиков", которые заняты тем, что любой десктопный разработчик под той же виндой делает вообще мимоходом, тратя в разы меньше времени.

        Кстати, когда еще был жив Blackberry, там интерфейсы именно так и делались - на QML с визуальным редактором. Но там в основе разработки был Qt.


        1. Vedomir
          10.04.2024 03:35
          +3

          Все-таки не совсем так. Тот же адаптивный интерфейс накидыванием контролов на форму так просто не сделаешь. Более-менее сложный дизайн, выходящий за рамки стандартной библиотеки элементов управления - тоже боль. Отсутствие встроенных шаблонов и стандартных архитектурных шаблонов вроде MVC приводило к заметному падению качества кода сложных программ, которые не исчерпываются одним окном. Это я уже не про DOS даже а про классический Windows и тот же Windows Forms.


        1. Layan
          10.04.2024 03:35
          +2

          Хотел бы я посмотреть, как десктопный разработчик мимоходом сделает интерфейс какой нибудь сложной CRM, да еще так, чтобы оно открывалось на любой устройстве - от телефонов, до ПК и телевизоров.


          1. domix32
            10.04.2024 03:35

            Особенно когда форма только с винды и запускалась.


          1. HemulGM
            10.04.2024 03:35

            Да без проблем, если честно. Особенно, если все формы не одинаково-шаблонные, которые web-фреймворки как бы автоматически строят


            1. Drucocu
              10.04.2024 03:35

              если все формы не одинаково-шаблонные, которые web-фреймворки как бы автоматически строят

              Человек рассуждает о формах в контексте интерфейсов. Ммм. Это такая эпичная иллюстрация эффекта Даннинга-Крюгера, что я даже затрудняюсь придумать наводящий вопрос.

              Ну вы походите по Интернету, чтоли, поизучайте разные сайты. GitHub, Gmail, VK, где у вас есть учётка, Хабр, опять же. Поизменяйте форму окна, посмотрите как они себя ведут. Откройте на телефоне. Подумайте, как вы это будете воспроизводить в своих любимых Делфях (да, я почитал другие комменты).


              1. HemulGM
                10.04.2024 03:35


                1. Drucocu
                  10.04.2024 03:35

                  Да видел, я, боже мой. Вы везде это таскаете.

                  Вы почему-то считаете, что если смогли воспроизвести базовую двухколоночную вёрстку, то теперь всё возможно.

                  Ну так это самый тривиальный layout, который ваши Делфи переняли из-за распространения в Интернете среди этих наших веб-фреймворков. А если нужно что-то интереснее?


                  1. HemulGM
                    10.04.2024 03:35
                    +1

                    Тогда в чем смысл вашего комментария? Вот пример с адаптивным интерфейсом. Делается это точно так же как делают адаптивные сайты. Только визуально.


                  1. HemulGM
                    10.04.2024 03:35
                    +1

                    Делфи перенял у веба) Смешно. Во-первых, здесь нет "двухколоночной верстки". Не надо предположений. То, что вы знаете натягивать на всё остальное - плохая идея. Принцип тут совершенно другой и не имеет значения, какой будет сложности интерфейс. Во-вторых, если визуальный редактор имеет все те же элементы, чем манипулируешь в декларативном подходе, то в чём сложность создавать такие же интерфейсы, но только визуально?

                    В этом примере сверху не накидано всё сразу на форму, интерфейс строится блоками, которые реализованы отдельно и могут работать независимо. Меню - один блок, область чата - другой, страницы настроек - другие блоки. Внимательнее бы смотрели "мои другие комментарии", то увидели бы, что каждый элемент этого приложения отображается отдельно в разных дизайнерах. А управлять блоками ими можно как угодно, помещай в слои разных назначений и с разными параметрами, удаляй, скрывай, создавай.

                    Создание GUI тут очень похоже на реализацию UI веб-сайтов. Есть макет, есть стили. Здесь точно так же. Помимо того, что он делится на блоки, каждый контрол может иметь свой собственный стиль (class в терминологии html/css). Создавать его можно тоже визуально. И каким бы сложным не был интерфейс, в любой момент можно изменить представление любого элемента и соответственно его поведение.


                    1. Drucocu
                      10.04.2024 03:35

                      Делфи перенял у веба) Смешно.

                      Создание GUI тут очень похоже на реализацию UI веб-сайтов.

                      У вас хобби такое - спорить с самим собой? Или это для меня шарада - найти взаимоисключающие параграфы?

                      То, что вы знаете натягивать на всё остальное - плохая идея.

                      Хотел вам написать буквально это. Вы варитесь в своей экосистеме - это хорошо и замечательно, что вы глубоко изучили свой инструментарий. Но вы, подобно комментатору выше, почему-то считаете, что все удобства и достижения существуют только в вашей области и напрочь игнорируете развитие индустрии в целом.


                      1. HemulGM
                        10.04.2024 03:35
                        +1

                        Делфи перенял у веба) Смешно.

                        Создание GUI тут очень похоже на реализацию UI веб-сайтов.

                        А представление контрола это придуманная вебом вещь? Я провел вам аналогию, чтоб было понятно. Ближе аналогия - скины и это близко не про веб. Принципы, использование в Делфи (речь о новом фреймворке, а не о старом со времен нулевых) не такие, как в вебе. Я лишь сказал, что создание GUI похоже на веб (и привел близкие аналогии), а не реализация. Вы хоть читайте текст.

                        считаете, что все удобства и достижения существуют только в вашей области и напрочь игнорируете развитие индустрии в целом

                        Ну да, ведь я не занимался разработкой GUI в декларативном варианте, я просто так спорю, без предметно.


                      1. Drucocu
                        10.04.2024 03:35

                        я просто так спорю, без предметно

                        Вы как раз спорите очень предметно, постоянно скатываясь в реализацию. С человеком, который за кодом не видит абстракций разговаривать действительно очень тяжело. Пожалуй, на этом остановлюсь.

                        Желаю вам успехов в работе и прошу прощения, если задел ваши чувства.


        1. Drucocu
          10.04.2024 03:35
          +1

          На самом деле ничего хорошего в этом нет.

          Какой вы загадочный человек - не перестаю удивляться. То у вас ООП не нужно, потому что появляется много лишних абстракций в коде, то слепо доверяете всякому WYSWYG пилить за вас вёрстку. Вы там как-то разберитесь уже.


        1. Neikist
          10.04.2024 03:35
          +2

          Ну вот есть для xml андроидного wyswig редактор. Но один черт написать разметку руками проще и быстрее чем ковыряться в визуальном редакторе с привязками, что во что вложено, свойствами и прочим. А уж не создавать а поддерживать - еще более просто руками.

          А compose тот же еще удобнее писать. Там визуального редактора конечно нет, но превью генерит.

          Все эти привязки и прочее напоминает мне 1с/делфи, причем даже не современный 1с, с управляемыми формами, а старый, с формами обычными. Как же от него плевался.


    1. mayorovp
      10.04.2024 03:35
      +5

      Вы слишком многого требуете от авторов библиотеки. Создать библиотеку и создать IDE - немного разные по сложности задачи.

      И уж если мы говорим про винду, то там для всего этого есть специальные .rc файлы, где хранится описание всего интерфейса и на работу с которыми заточен WinAPI.

      Эти .rc файлы даже не всеми виндовыми приложениями используются: у Delphi VCL свой механизм, у WinForms свой, у WPF свой - и это только то с чем я работал лично. И ладно бы это был хотя бы удобный инструмент, так ведь нет - это был ужасный инструмент, единственным достоинством которого является его "изкоробочность".

      Писать руками мало-мальски сложные интерфейсы таким вот образом - 80% времени на это уйдет. Еще и, подишто, на бумажке придется все это рисовать, если у вас на форме пара десятков контролов (менюшки, списки, выпадающие списки, движки, счетчики, текстовые формы с валидацией ввода, футбары с иконками, статусбыры в какими-то контролами и т.п.)

      Иногда лучше и правда рисовать на бумажке.

      У визуальных редакторов, при всей простоте в создании интерфейса, я каждый раз наблюдал дикие сложности в редактировании, особенно на чужих проектах. Вот нужно заменить 14й шрифт на 12й? Сиди как дурак, тыкай абсолютно все элементы интерфейса, и проверяй размер шрифта. И тыкай аккуратно, чтобы случайно не тыкнуть два раза...

      Разумеется, я так на самом деле ни разу не делал - я просто открывал текстовый файл с описанием интерфейса и менял всё там. Поэтому текстовой разметке - быть!


      1. HemulGM
        10.04.2024 03:35
        +1

        Ну так ведь подходы к дизайнеру могут быть разными. Например, тот же пример с шрифтами. В Delphi, если тебе нужно у всех контролов поменять шрифт, то достаточно поменять шрифт родителя (например, формы). Шрифт, как и многие свойства контролов В Delphi поддерживают наследование. (тут речь о VCL)

        Или можно взять новый подход, который сейчас имеется в Delphi для кроссплатформенного фреймворка (FMX). Там есть понятие стилей и оно сопоставимо со стилями CSS. Т.е. для представления любого контрола мы можем задать стиль и достаточно поменять настройки стиля, чтобы изменить все контролы с этим стилем.

        На основании этого, я даже делал полноценный стиль Material Design с возможностью указать акцент и смену темная/светлая тема.

        При всём при этом у нас остается визуальный дизайнер и кстати, дизайн стиля мы тоже создаем визуально, а не кодом как css. Т.е. всё что мы настраиваем и создаем в виде стилей сразу применяется и отображается в дизайнере.


        1. mayorovp
          10.04.2024 03:35
          +2

          Шрифт, как и многие свойства контролов В Delphi поддерживают наследование. (тут речь о VCL)

          В своём коде я так и делал. Но при редактировании чужой программы всё равно нужно проверить все элементы управления, потому что, тот кто их создавал, мог поставить им размер шрифта вручную.

          И со стилями тоже самое. Свои стили я пытаюсь организовать в некоторую систему, а вот чужие регулярно оказывались той ещё хренью.


          1. HemulGM
            10.04.2024 03:35

            С чужой программой проблемы будут с любым подходом. В дизайнере можно хотя бы выделить все кнопки и задать значение, а в декларативном, если были заданы константные значения разным контролам, то придется попотеть, чтобы вынести все значения в константы


            1. mayorovp
              10.04.2024 03:35

              В текстовом редакторе у меня есть хотя бы поиск. А если это редактор кода - то и Find Usages.


              1. HemulGM
                10.04.2024 03:35
                +1

                Ну текстовое представление формы в Delphi тоже можно увидеть и редактировать)


      1. SpiderEkb
        10.04.2024 03:35

        Все верно. Тонкости в тексте проще довести. Но быстро собрать форму из нескольких десятков элементов проще визуально. А потом уже допилить руками в тексте. Чтобы не сидеть и не представлять в голове "вот у меня кнопка, левый верхний угол X, Y пунктов, ширина N, M пунктов, дальше интервал K пунктов - сколько там должно быть X1, Y1 для второй кнопки? А при ширине N1, M1 оно за границу формы не вылезет?"


        1. mayorovp
          10.04.2024 03:35
          +4

          А вот теперь уже я должен спросить - извините, какой сейчас год?

          Современные UI фреймворки не требуют абсолютного позиционирования элементов. Вы просто добавляете в контейнер сначала одну кнопку, а потом вторую.


          1. HemulGM
            10.04.2024 03:35

            Пусть и без абсолютного позиционирования, но видеть наглядно "как получилось" гораздо удобнее, чем перезагружать композер или всю программу.

            Не будь такой необходимости, вряд ли бы появились на свет такие инструменты как Figma


            1. mayorovp
              10.04.2024 03:35

              Про удобство не спорю. Но вот с дихотомией "либо визуальный редактор, либо рассчитывать все координаты" я не согласен категорически.

              И вообще, наиболее удобный вариант редактора я видел для WPF - на одной панели разметка, на второй панели результат.


              1. HemulGM
                10.04.2024 03:35

                А я не вижу смысла в отдельной панели с кодом, когда есть дизайнер, в котором ты можешь расположить всё как надо. Речь конечно не об абсолютном позиционировании (я уже не помню, когда подобным занимался), а о добавлении слоев, вставке контролов, указании привязок и настройке других свойств, наличие которых не нужно гуглить или смотреть справку по контролу (виджету).

                Hidden text


                1. mayorovp
                  10.04.2024 03:35

                  Чтобы видеть всё то, чего визуальный редактор не показывает.

                  Вот на вашем втором скриншоте (или это первый? как их вообще считать если один из них под спойлером) текст "Make HTTP Request in javascript" - это статический текст или результат биндинга? У левой панели ширина автоматическая, фиксированная или в процентах? Размер шрифта унаследован, объявлен в стиле, или установлен?


                  1. HemulGM
                    10.04.2024 03:35
                    +1

                    Ну так ведь достаточно нажать на то, что вас интересует и увидеть свойства.


                    1. mayorovp
                      10.04.2024 03:35

                      Ну, собственно, в этом и проблема - нужно нажать чтобы увидеть.


                      1. HemulGM
                        10.04.2024 03:35

                        Это не проблема, в wpf ты точно так же делаешь. Сомневаюсь, что всегда ищешь элемент в xaml


                      1. mayorovp
                        10.04.2024 03:35
                        +2

                        Конкретно в WPF мне было прозе писать XAML чем пытаться положить компонент в нужное место мышкой, особенно когда в этом месте уже лежал "бутерброд" из трёх других компонентов.


                      1. zzzzzzerg
                        10.04.2024 03:35

                        Подтверждаю, в нашем опыте тоже было проще писать XAML, а если уж использовался PRISM - то тем более.

                        Но даже если XAML писался "вручную" - всегда можно было открыть параллельно WYSIWYG редактор в студии и убедиться, что View компонуется так как и ожидается.


      1. zinpub
        10.04.2024 03:35

        Вот нужно заменить 14й шрифт на 12й? Сиди как дурак, тыкай абсолютно все элементы интерфейса, и проверяй размер шрифта. И тыкай аккуратно, чтобы случайно не тыкнуть два раза...

        Ну откуда вы это берёте? Конечно же просто изменяете стиль, в одном месте


        1. mayorovp
          10.04.2024 03:35

          Это если автор кода заранее позаботился о удобстве внесения изменений. Но я встречал коллег, которые с мотивацией "а вдруг оно посчитается неправильно и окажется не таким как в фигме?!" не использовали никаких стилей.


    1. Vedomir
      10.04.2024 03:35
      +1

      Да, было. Но ушло в далекое прошлое и превратилось в забытую технологию предков. Не исключено, что лет через пять возродится и породит гигантскую волну хайпа как уникальное и не имеющее аналогов нововведение. Примерно как статическая типизация с TypeScript, автоматическая перестройка интерфейса по изменениям в модели в SPA-фреймворках или компиляция в один исполняемый файл с Rust и Go. На всякий случай - я не хочу сказать что во всех вышеперечисленных технологиях нет ничего нового и/или отличающегося от упомянутого мной вообще.


  1. stanukih
    10.04.2024 03:35
    +1

    А где итоговый скриншот? На днях тоже ковырял. Был очень раздосадован, когда провозился несколько часов над демопроектом, выполняя все строго по инструкции. Оказалось, что по автомату подцепилась версия slint 1.4, когда текущая 1.5. После обновления, все завелось без правок кода.

    В целом проект очень интересный, в том числе, с его возможностью сборки под веб. К стати, собрался ли этот проект под веб?


    1. orekh
      10.04.2024 03:35

      Вижу анимацию перед разделом про браузерную версию.


    1. Ertanic Автор
      10.04.2024 03:35
      +1

      Спасибо, что заметили пропажу. Отпинал Жулика и вернул гифку на место (●'◡'●)


  1. kovalensky
    10.04.2024 03:35

    Как я понимаю slint был создан для проектирования маленьких интерфейсов. В HMI панелях к примеру. А что насчёт iced?


    1. domix32
      10.04.2024 03:35

      Их там слишком много всяких. В итоге те формы появятся не в .slint файлах, а прямо в коде, так что врядли разница будет большой.


  1. domix32
    10.04.2024 03:35

    Gettext через макросы,

    Оно скорее всего под фичей спрятано, вот и не завелось.


    1. Ertanic Автор
      10.04.2024 03:35
      +1

      Да нет, указывал фичю, иначе компилятор ругается :)


  1. aem
    10.04.2024 03:35

    Удивился и порадовался, когда увидел калькулятор на RUST :)

    @Ertanic , а подскажите какой размер в кб бинарника и wasm вышел?

    Я как-то игрался с egui и получил бинарь windows-msvc на 2.82 MB. А для веба wasm = 1.1 MB. Что не так уж мало.


    1. Ertanic Автор
      10.04.2024 03:35

      3,72 мб бинарь windows-msvc , 2,53 мб в wasm. Утыкал максимальными оптимизациями и поудалял ненужные ресурсы.


  1. abdsh
    10.04.2024 03:35

    Круто


  1. stanukih
    10.04.2024 03:35

    Я так и не понял, как заставить его растягиваться, так как автоматом канвасу выставляются те же размеры, что указаны в width и height

    Скорее всего это ещё не реализовано. Даже в их текстовом проекте https://releases.slint.dev/1.5.1/demos/memory/

    явно ничего никуда не растягивается


  1. svfox
    10.04.2024 03:35

    А как на андроид SQLite завести используя rust ? Тудушку под SQLite сделал, при запуске под Андроид не создаётся бд,


    1. Ertanic Автор
      10.04.2024 03:35

      На ПК создаётся бд? И, пожалуйста, уточните крейт, который вы используете для работы с SQLite.


      1. svfox
        10.04.2024 03:35

        На ПК создаётся, использую rusqlite = { version = "0.31.0", features = ["bundled"] }


    1. HemulGM
      10.04.2024 03:35
      +1

      Смотри, куда пытаешься создать бд. В Андроид с расположением файлов куда строже, чем в Винде. Нужны права, нужны правильные пути