/// TEMPORARY AI GENERATED FRONTEND \\\ /// NOT TO BE USED IN PRODUCTION \\\ import { Theme } from "../theme.slint"; import { Icons } from "../icons.slint"; import { Icon } from "icon.slint"; import { ToolButton } from "tool-button.slint"; // Message box. Enter sends, Shift+Enter inserts a newline (same as Discord). // The component is deliberately stateless about *what* is being sent: the // parent owns the reply/edit state and just tells us what banner to show. export component Composer inherits Rectangle { in property placeholder; in property banner; // "Replying to Alice" / "Editing message"; "" hides it in property editing; in property typing-text; // "Bob is typing…" callback submit(string); callback cancel-banner(); callback clear-text(); callback typing(); callback attach(); // Parent -> composer requests, as properties so the composer can live inside an `if`. in property text-to-set; in property text-nonce; // bump to replace the input text with `text-to-set` (and focus) in property focus-nonce; // bump to just focus the input changed text-nonce => { input.text = root.text-to-set; input.focus(); } changed focus-nonce => { input.focus(); } // Grows with content up to ~8 lines, like Discord. property input-height: min(max(input.preferred-height, 24px), 180px); height: (root.banner != "" ? 32px : 0px) + input-height + 24px + 24px; background: Theme.chat-bg; // Reply / edit banner sits above the input and shares its rounded top. if root.banner != "": Rectangle { x: 16px; y: 0px; width: parent.width - 32px; height: 32px + 8px; background: Theme.sidebar-bg; border-top-left-radius: 8px; border-top-right-radius: 8px; Text { x: 12px; width: parent.width - 60px; height: 32px; text: root.banner; color: Theme.text-muted; font-size: 13px; vertical-alignment: center; overflow: elide; } ToolButton { glyph: Icons.close; x: parent.width - self.width - 6px; y: 2px; clicked => { root.cancel-banner(); } } } box := Rectangle { x: 16px; y: (root.banner != "" ? 32px : 0px); width: parent.width - 32px; height: root.input-height + 24px; background: Theme.input-bg; border-radius: root.banner != "" ? 0px : 8px; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px; ToolButton { glyph: Icons.plus-circle; x: 6px; y: parent.height - self.height - 10px; clicked => { root.attach(); } } FocusScope { x: 44px; y: 12px; width: parent.width - 44px - 52px; height: root.input-height; // Intercept Enter before the TextInput turns it into a newline. capture-key-pressed(event) => { if (event.text == Key.Return && !event.modifiers.shift) { if (input.text != "") { root.submit(input.text); input.text = ""; } return accept; } return reject; } input := TextInput { width: parent.width; height: parent.height; single-line: false; wrap: word-wrap; color: Theme.text-normal; font-size: 15px; selection-background-color: Theme.accent; edited => { root.typing(); } } } if input.text == "": Text { x: 44px; y: 12px; width: parent.width - 100px; height: 24px; text: root.placeholder; color: Theme.text-faint; font-size: 15px; vertical-alignment: center; overflow: elide; } ToolButton { glyph: Icons.smile; x: parent.width - self.width - 8px; y: parent.height - self.height - 10px; // TODO: emoji picker } } // Typing indicator, under the box. Text { x: 20px; y: parent.height - 22px; width: parent.width - 40px; height: 18px; text: root.typing-text; color: Theme.text-muted; font-size: 12px; font-weight: 600; vertical-alignment: center; overflow: elide; } }