mirror of
https://github.com/slint-ui/slint.git
synced 2025-10-23 00:32:46 +00:00

Some checks are pending
autofix.ci / format_fix (push) Waiting to run
autofix.ci / lint_typecheck (push) Waiting to run
CI / files-changed (push) Waiting to run
CI / build_and_test (--exclude bevy-example, ubuntu-22.04, 1.85) (push) Blocked by required conditions
CI / build_and_test (--exclude ffmpeg --exclude gstreamer-player, --exclude bevy-example, windows-2022, 1.85) (push) Blocked by required conditions
CI / build_and_test (--exclude ffmpeg --exclude gstreamer-player, macos-14, stable) (push) Blocked by required conditions
CI / build_and_test (--exclude ffmpeg --exclude gstreamer-player, windows-2022, beta) (push) Blocked by required conditions
CI / build_and_test (--exclude ffmpeg --exclude gstreamer-player, windows-2022, stable) (push) Blocked by required conditions
CI / build_and_test (ubuntu-22.04, nightly) (push) Blocked by required conditions
CI / node_test (macos-14) (push) Blocked by required conditions
CI / node_test (ubuntu-22.04) (push) Blocked by required conditions
CI / node_test (windows-2022) (push) Blocked by required conditions
CI / python_test (macos-14) (push) Blocked by required conditions
CI / python_test (ubuntu-22.04) (push) Blocked by required conditions
CI / python_test (windows-2022) (push) Blocked by required conditions
CI / cpp_test_driver (macos-13) (push) Blocked by required conditions
CI / cpp_test_driver (ubuntu-22.04) (push) Blocked by required conditions
CI / cpp_test_driver (windows-2022) (push) Blocked by required conditions
CI / cpp_cmake (macos-14, 1.85) (push) Blocked by required conditions
CI / cpp_cmake (ubuntu-22.04, stable) (push) Blocked by required conditions
CI / cpp_cmake (windows-2022, nightly) (push) Blocked by required conditions
CI / cpp_package_test (push) Blocked by required conditions
CI / vsce_build_test (push) Blocked by required conditions
CI / mcu (pico-st7789, thumbv6m-none-eabi) (push) Blocked by required conditions
CI / mcu (pico2-st7789, thumbv8m.main-none-eabihf) (push) Blocked by required conditions
CI / mcu (stm32h735g, thumbv7em-none-eabihf) (push) Blocked by required conditions
CI / mcu-embassy (push) Blocked by required conditions
CI / ffi_32bit_build (push) Blocked by required conditions
CI / docs (push) Blocked by required conditions
CI / wasm (push) Blocked by required conditions
CI / wasm_demo (push) Blocked by required conditions
CI / tree-sitter (push) Blocked by required conditions
CI / updater_test (0.3.0) (push) Blocked by required conditions
CI / fmt_test (push) Blocked by required conditions
CI / esp-idf-quick (push) Blocked by required conditions
CI / android (push) Blocked by required conditions
CI / miri (push) Blocked by required conditions
CI / test-figma-inspector (push) Blocked by required conditions
476 lines
20 KiB
Text
476 lines
20 KiB
Text
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
|
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
|
|
|
// cSpell: ignore resizer
|
|
|
|
import { Button, ComboBox, HorizontalBox, LineEdit, ListView, Palette, ScrollView, VerticalBox } from "std-widgets.slint";
|
|
import { Api, ComponentItem, DiagnosticSummary, DropMark, LayoutKind, Selection, SelectionRectangle } from "../api.slint";
|
|
import { Resizer } from "../components/resizer.slint";
|
|
import { Group, GroupHeader } from "../components/group.slint";
|
|
import { SelectionPopup } from "../components/selection-popup.slint";
|
|
import { StatusLineApi } from "../components/status-line.slint";
|
|
import { EditorPalette } from "../components/styling.slint";
|
|
|
|
global PreviewState {
|
|
out property <length> minimum-preview-size: 16px;
|
|
}
|
|
|
|
enum SelectionKind {
|
|
none,
|
|
select_at,
|
|
select_up_or_down,
|
|
}
|
|
|
|
export enum DrawAreaMode {
|
|
uninitialized,
|
|
viewing,
|
|
selecting,
|
|
outdated,
|
|
}
|
|
|
|
component SelectionFrame {
|
|
in property <Selection> selection;
|
|
in property <bool> is-primary;
|
|
in property <bool> interactive: true;
|
|
|
|
function pick-selection-color(selection: Selection) -> color {
|
|
if selection.layout-data != LayoutKind.None {
|
|
if is-primary {
|
|
return EditorPalette.layout-element-selection-primary;
|
|
} else {
|
|
return EditorPalette.layout-element-selection-secondary;
|
|
}
|
|
} else if selection.is-interactive {
|
|
if is-primary {
|
|
return EditorPalette.interactive-element-selection-primary;
|
|
} else {
|
|
return EditorPalette.interactive-element-selection-secondary;
|
|
}
|
|
} else {
|
|
if is-primary {
|
|
return EditorPalette.general-element-selection-primary;
|
|
} else {
|
|
return EditorPalette.general-element-selection-secondary;
|
|
}
|
|
}
|
|
}
|
|
|
|
private property <color> selection-color: pick-selection-color(root.selection);
|
|
|
|
property <bool> had-drag-distance: false;
|
|
|
|
callback resize(x: length, y: length, width: length, height: length);
|
|
callback can-move-to(x: length, y: length, mouse-x: length, mouse-y: length) -> bool;
|
|
callback move-to(x: length, y: length, mouse-x: length, mouse-y: length);
|
|
callback select-through(x: length, y: length, enter-component: bool, reverse: bool);
|
|
callback selection-stack-at(x: length, y: length);
|
|
callback selected-element-delete();
|
|
|
|
if !root.interactive || !is-primary: Rectangle {
|
|
x: 0;
|
|
y: 0;
|
|
border-color: root.selection-color;
|
|
border-width: 1px;
|
|
}
|
|
|
|
if root.interactive && is-primary: Resizer {
|
|
clicked(x, y, modifiers) => {
|
|
key-handler.focus();
|
|
}
|
|
|
|
double-clicked(x, y, modifiers) => {
|
|
root.select-through(root.x + x, root.y + y, modifiers.control, modifiers.shift);
|
|
}
|
|
|
|
changed has-hover => {
|
|
if self.has-hover {
|
|
StatusLineApi.help-text = @tr("<right-click> show selection popup, <double-click> select behind element, <{}> ignores component boundaries", Api.control-key-name);
|
|
} else {
|
|
StatusLineApi.help-text = "";
|
|
}
|
|
}
|
|
|
|
pointer-event(x, y, event) => {
|
|
if event.button == PointerEventButton.right && event.kind == PointerEventKind.down {
|
|
root.selection-stack-at(root.x + x, root.y + y);
|
|
}
|
|
}
|
|
|
|
is-moveable: root.selection.is-moveable;
|
|
is-resizable: root.selection.is-resizable;
|
|
|
|
x-position: root.x;
|
|
y-position: root.y;
|
|
|
|
color: root.selection-color;
|
|
x: 0;
|
|
y: 0;
|
|
width: root.width;
|
|
height: root.height;
|
|
|
|
resize(x, y, w, h, done) => {
|
|
root.x = x;
|
|
root.y = y;
|
|
root.width = w;
|
|
root.height = h;
|
|
if done {
|
|
root.resize(x, y, w, h);
|
|
}
|
|
}
|
|
|
|
can-move-to(x, y, mx, my) => {
|
|
root.had-drag-distance = abs((root.x - x) / 1px) > 8 || abs((root.y - y) / 1px) > 8 || root.had-drag-distance;
|
|
|
|
if root.had-drag-distance {
|
|
root.x = x;
|
|
root.y = y;
|
|
return root.can-move-to(x, y, mx, my);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
move-to(x, y, mx, my) => {
|
|
root.had-drag-distance = abs((root.x - x) / 1px) > 8 || abs((root.y - y) / 1px) > 8 || root.had-drag-distance;
|
|
|
|
if root.had-drag-distance {
|
|
root.x = x;
|
|
root.y = y;
|
|
root.move-to(x, y, mx, my);
|
|
}
|
|
root.had-drag-distance = false;
|
|
}
|
|
|
|
inner := Rectangle {
|
|
border-color: root.selection-color;
|
|
border-width: 1px;
|
|
background: parent.resizing || parent.moving ? root.selection-color.with-alpha(0.5) : root.selection-color.with-alpha(0.0);
|
|
}
|
|
|
|
key-handler := FocusScope {
|
|
enabled <=> root.interactive;
|
|
|
|
init => {
|
|
self.focus();
|
|
}
|
|
|
|
key-pressed(event) => {
|
|
if event.text == Key.Delete {
|
|
Api.selected-element-delete();
|
|
return accept;
|
|
}
|
|
reject
|
|
}
|
|
}
|
|
}
|
|
|
|
// Size label:
|
|
if selection.is-resizable && is-primary && interactive: Rectangle {
|
|
x: (root.width - label.width) * 0.5;
|
|
y: root.height + 3px;
|
|
width: label.width;
|
|
height: label.height;
|
|
|
|
label := Rectangle {
|
|
background: root.selection-color;
|
|
width: label-text.width * 1.2;
|
|
height: label-text.height * 1.2;
|
|
label-text := Text {
|
|
color: Colors.white;
|
|
text: Math.round(root.width / 1px) + "x" + Math.round(root.height / 1px);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export component PreviewView inherits Rectangle {
|
|
property <[SelectionRectangle]> selections: Api.highlight-positions(Api.current-element.source-uri, Api.current-element.offset);
|
|
in property <ComponentItem> visible-component;
|
|
property <DiagnosticSummary> diagnostic-summary <=> Api.diagnostic-summary;
|
|
out property <bool> preview-is-current: self.diagnostic-summary != DiagnosticSummary.Errors;
|
|
|
|
property <DropMark> drop-mark <=> Api.drop-mark;
|
|
property <component-factory> preview-area <=> Api.preview-area;
|
|
in-out property <bool> select-mode: false;
|
|
|
|
out property <bool> preview-visible: preview-area-container.has-component;
|
|
out property <DrawAreaMode> mode: uninitialized;
|
|
|
|
preferred-height: max(max(preview-area-container.preferred-height, preview-area-container.min-height) + 2 * scroll-view.border, 10 * scroll-view.border);
|
|
preferred-width: max(max(preview-area-container.preferred-width, preview-area-container.min-width) + 2 * scroll-view.border, 10 * scroll-view.border);
|
|
|
|
changed diagnostic-summary => {
|
|
if self.diagnostic-summary == DiagnosticSummary.NothingDetected {
|
|
StatusLineApi.help-text = @tr("");
|
|
} else if self.diagnostic-summary == DiagnosticSummary.Warnings {
|
|
StatusLineApi.help-text = @tr("Check your text editor for warnings");
|
|
}
|
|
if self.diagnostic-summary == DiagnosticSummary.Errors {
|
|
StatusLineApi.help-text = @tr("Check your text editor for errors");
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
scroll-view := ScrollView {
|
|
out property <length> border: 30px;
|
|
|
|
width: 100%;
|
|
height: 100%;
|
|
viewport-width: drawing-rect.width;
|
|
viewport-height: drawing-rect.height;
|
|
|
|
drawing-rect := Rectangle {
|
|
width: max(scroll-view.visible-width, main-resizer.width + scroll-view.border);
|
|
height: max(scroll-view.visible-height, main-resizer.height + scroll-view.border);
|
|
background: Palette.background;
|
|
Image {
|
|
width: 100%;
|
|
height: 100%;
|
|
source: @image-url("../assets/background.svg");
|
|
vertical-tiling: repeat;
|
|
horizontal-tiling: repeat;
|
|
vertical-alignment: top;
|
|
horizontal-alignment: left;
|
|
colorize: Palette.alternate-background;
|
|
}
|
|
|
|
unselect-area := TouchArea {
|
|
clicked => {
|
|
Api.unselect();
|
|
StatusLineApi.help-text = "";
|
|
}
|
|
mouse-cursor: root.mode == DrawAreaMode.selecting ? MouseCursor.crosshair : MouseCursor.default;
|
|
|
|
changed has-hover => {
|
|
if self.has-hover && root.selections.length > 0 {
|
|
StatusLineApi.help-text = "<click> unselect";
|
|
} else {
|
|
StatusLineApi.help-text = "";
|
|
}
|
|
}
|
|
}
|
|
|
|
unselect-area-blocker := TouchArea {
|
|
x: content-border.x;
|
|
y: content-border.y;
|
|
width: content-border.width;
|
|
height: content-border.height;
|
|
|
|
mouse-cursor: root.mode == DrawAreaMode.selecting ? MouseCursor.crosshair : MouseCursor.default;
|
|
}
|
|
|
|
content-border := Rectangle {
|
|
x: main-resizer.x + (main-resizer.width - self.width) / 2;
|
|
y: main-resizer.y + (main-resizer.height - self.height) / 2;
|
|
width: main-resizer.width + 2 * self.border-width;
|
|
height: main-resizer.height + 2 * self.border-width;
|
|
border-width: 1px;
|
|
border-color: Palette.border;
|
|
}
|
|
|
|
main-resizer := Resizer {
|
|
private property <component-factory> component-factory <=> preview-area-container.component-factory;
|
|
private property <length> target-width;
|
|
private property <length> target-height;
|
|
|
|
color: root.select-mode ? Colors.black : Colors.transparent;
|
|
fill-color: root.select-mode ? Colors.white : Colors.transparent;
|
|
|
|
is-moveable: false;
|
|
is-resizable <=> preview-area-container.is-resizable;
|
|
|
|
x-position: parent.x;
|
|
y-position: parent.y;
|
|
|
|
resize(_, _, w, h) => {
|
|
resize-to-preview-constraints(w, h);
|
|
}
|
|
width: preview-area-container.width;
|
|
height: preview-area-container.height;
|
|
|
|
changed component-factory => {
|
|
if Api.resize-to-preferred-size {
|
|
self.target-width = preview-area-container.preferred-width.abs() < 0.5px ? drawing-rect.width - scroll-view.border : preview-area-container.preferred-width;
|
|
self.target-height = preview-area-container.preferred-height.abs() < 0.5px ? drawing-rect.height - scroll-view.border : preview-area-container.preferred-height;
|
|
|
|
resize-to-preview-constraints(self.target-width, self.target-height);
|
|
} else {
|
|
resize-to-preview-constraints(preview-area-container.width, preview-area-container.height);
|
|
}
|
|
|
|
Api.resize-to-preferred-size = false;
|
|
|
|
if Api.focus-previewed-element {
|
|
preview-area-container.focus();
|
|
}
|
|
Api.focus-previewed-element = false;
|
|
}
|
|
|
|
function resize-to-preview-constraints(width: length, height: length) {
|
|
preview-area-container.width = clamp(width, max(preview-area-container.min-width, PreviewState.minimum-preview-size), max(preview-area-container.max-width, PreviewState.minimum-preview-size));
|
|
preview-area-container.height = clamp(height, max(preview-area-container.min-height, PreviewState.minimum-preview-size), max(preview-area-container.max-height, PreviewState.minimum-preview-size));
|
|
}
|
|
|
|
Rectangle {
|
|
clip: true;
|
|
|
|
HorizontalLayout {
|
|
preview-area-container := ComponentContainer {
|
|
property <bool> is-resizable: (self.min-width != self.max-width || self.min-height != self.max-height) && self.has-component;
|
|
|
|
component-factory: root.preview-area;
|
|
|
|
// The width and the height can't depend on the layout info of the inner item otherwise this would
|
|
// cause a recursion if this happens (#3989)
|
|
// Instead, we use a init function to initialize
|
|
width: 0px;
|
|
height: 0px;
|
|
|
|
init => {
|
|
self.width = max(self.preferred-width, self.min-width);
|
|
self.height = max(self.preferred-height, self.min-height);
|
|
}
|
|
changed min-width => {
|
|
main-resizer.resize-to-preview-constraints(self.width, self.height);
|
|
}
|
|
|
|
changed max-width => {
|
|
main-resizer.resize-to-preview-constraints(self.width, self.height);
|
|
}
|
|
|
|
changed width => {
|
|
Api.reselect();
|
|
}
|
|
|
|
changed height => {
|
|
Api.reselect();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
selection-area := TouchArea {
|
|
private property <length> selection-x: 0px;
|
|
private property <length> selection-y: 0px;
|
|
private property <SelectionKind> selection-kind: SelectionKind.none;
|
|
|
|
clicked => {
|
|
self.selection-x = self.pressed-x;
|
|
self.selection-y = self.pressed-y;
|
|
self.selection-kind = SelectionKind.select-at;
|
|
}
|
|
double-clicked => {
|
|
self.selection-x = self.pressed-x;
|
|
self.selection-y = self.pressed-y;
|
|
self.selection-kind = SelectionKind.select-up-or-down;
|
|
}
|
|
pointer-event(event) => {
|
|
// This needs to fire AFTER clicked and double-clicked to work :-/
|
|
if (event.kind == PointerEventKind.up && event.button == PointerEventButton.left) {
|
|
if (self.selection-kind == SelectionKind.select_up_or_down) {
|
|
Api.select-behind(self.selection-x, self.selection-y, event.modifiers.control, event.modifiers.shift);
|
|
} else if (self.selection-kind == SelectionKind.select-at) {
|
|
Api.select-at(self.selection-x, self.selection-y, event.modifiers.control);
|
|
}
|
|
} else if (event.kind == PointerEventKind.down && event.button == PointerEventButton.right) {
|
|
self.selection-x = self.mouse-x;
|
|
self.selection-y = self.mouse-y;
|
|
self.selection-kind = SelectionKind.select-at;
|
|
|
|
selection-popup.show-selection-stack(self.selection-x, self.selection-y);
|
|
} else if (event.kind == PointerEventKind.up && event.button == PointerEventButton.right) {
|
|
self.selection-kind = SelectionKind.none;
|
|
}
|
|
self.selection-kind = SelectionKind.none;
|
|
}
|
|
mouse-cursor: crosshair;
|
|
enabled: root.mode == DrawAreaMode.selecting;
|
|
|
|
changed has-hover => {
|
|
if self.has-hover && self.enabled {
|
|
StatusLineApi.help-text = @tr("<click> select element in current component, <right-click> to select interactively, <{}-click> to select an element in any component", Api.control-key-name);
|
|
} else {
|
|
StatusLineApi.help-text = "";
|
|
}
|
|
}
|
|
}
|
|
|
|
DropArea {
|
|
can-drop(event) => {
|
|
if event.mime-type != "application/x-slint-component" {
|
|
return false;
|
|
}
|
|
return Api.can-drop(event.data.to-float(), event.position.x, event.position.y, true);
|
|
}
|
|
dropped(event) => {
|
|
Api.drop(event.data.to-float(), event.position.x, event.position.y);
|
|
}
|
|
}
|
|
|
|
selection-popup := SelectionPopup {
|
|
preview-width: preview-area-container.width;
|
|
preview-height: preview-area-container.height;
|
|
x: 0px;
|
|
y: 0px;
|
|
max-popup-height: root.height * 0.9;
|
|
}
|
|
|
|
selection-display-area := Rectangle {
|
|
for s[idx] in root.selections: SelectionFrame {
|
|
x: s.x;
|
|
y: s.y;
|
|
width: s.width;
|
|
height: s.height;
|
|
interactive: root.mode == DrawAreaMode.selecting;
|
|
selection: Api.selection;
|
|
is-primary: self.selection.highlight-index == idx;
|
|
resize(x, y, w, h) => {
|
|
Api.selected-element-resize(x, y, w, h);
|
|
}
|
|
can-move-to(x, y, mx, my) => {
|
|
Api.selected-element-can-move-to(x, y, mx, my);
|
|
}
|
|
move-to(x, y, mx, my) => {
|
|
Api.selected-element-move(x, y, mx, my);
|
|
}
|
|
selected-element-delete() => {
|
|
Api.selected-element-delete();
|
|
}
|
|
select-through(x, y, c, f) => {
|
|
Api.select-behind(x, y, c, f);
|
|
}
|
|
selection-stack-at(x, y) => {
|
|
selection-popup.show-selection-stack(x, y);
|
|
}
|
|
}
|
|
}
|
|
|
|
if drop-mark.x1 >= 0.0 || drop-mark.y1 >= 0.0 || drop-mark.x2 >= 0.0 || drop-mark.y2 >= 0.0: Rectangle {
|
|
x: drop-mark.x1;
|
|
y: drop-mark.y1;
|
|
width: drop-mark.x2 - drop-mark.x1;
|
|
height: drop-mark.y2 - drop-mark.y1;
|
|
|
|
border-color: EditorPalette.drop-mark-foreground;
|
|
background: EditorPalette.drop-mark-background;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
states [
|
|
uninitialized when !preview-area-container.has-component: {
|
|
root.mode: DrawAreaMode.uninitialized;
|
|
}
|
|
error when !root.preview-is-current && preview-area-container.has-component: {
|
|
root.mode: DrawAreaMode.outdated;
|
|
}
|
|
selecting when root.select-mode && root.preview-is-current && preview-area-container.has-component: {
|
|
root.mode: DrawAreaMode.selecting;
|
|
}
|
|
viewing when !root.select-mode && root.preview-is-current && preview-area-container.has-component: {
|
|
root.mode: DrawAreaMode.viewing;
|
|
}
|
|
]
|
|
}
|