diff --git a/.gitignore b/.gitignore index e873887..8e9b114 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ -/target -.qtcreator -.idea +**/target/** +**/.qtcreator/** +**/.idea/** +**/*.autosave/** \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 10a9bbf..3574567 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,6 +62,7 @@ dependencies = [ "cxx-qt", "cxx-qt-build", "cxx-qt-lib", + "log", ] [[package]] @@ -266,6 +267,12 @@ dependencies = [ "cc", ] +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + [[package]] name = "memchr" version = "2.7.4" diff --git a/Cargo.toml b/Cargo.toml index a10ab35..8b7c81c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" cxx = "1.0.95" cxx-qt = "0.7" cxx-qt-lib = { version="0.7", features = ["qt_full"] } +log = { version = "0.4.27", features = [] } [build-dependencies] # The link_qt_object_files feature is required for statically linking Qt 6. diff --git a/build.rs b/build.rs index e14579a..4ddd535 100644 --- a/build.rs +++ b/build.rs @@ -9,7 +9,7 @@ fn main() { // - Qt Qml requires linking Qt Network on macOS .qt_module("Network") .qml_module(QmlModule { - uri: "test", + uri: "clide.module", rust_files: &["src/main.rs"], qml_files: &["qml/main.qml", "qml/Menu/ClideMenu.qml", diff --git a/qml/Menu/ClideMenuBar.qml.autosave b/qml/Menu/ClideMenuBar.qml.autosave new file mode 100644 index 0000000..f252803 --- /dev/null +++ b/qml/Menu/ClideMenuBar.qml.autosave @@ -0,0 +1,140 @@ +import QtQuick +import QtQuick.Controls + +MenuBar { + background: Rectangle { + color: "#3c3f41" + border.color: "#575757" + } + + Action { + id: actionNewProject + + text: qsTr("&New Project...") + } + Action { + id: actionOpen + + text: qsTr("&Open...") + } + Action { + id: actionSave + + text: qsTr("&Save") + } + Action { + id: actionExit + + text: qsTr("&Exit") + + onTriggered: Qt.quit() + } + ClideMenu { + title: qsTr("&File") + + ClideMenuBarItem { + action: actionNewProject + } + ClideMenuBarItem { + action: actionOpen + } + ClideMenuBarItem { + action: actionSave + } + MenuSeparator { + background: Rectangle { + border.color: color + color: "#3c3f41" + implicitHeight: 3 + implicitWidth: 200 + } + } + ClideMenuBarItem { + action: actionExit + } + } + Action { + id: actionUndo + + text: qsTr("&Undo") + } + Action { + id: actionRedo + + text: qsTr("&Redo") + } + Action { + id: actionCut + + text: qsTr("&Cut") + } + Action { + id: actionCopy + + text: qsTr("&Copy") + } + Action { + id: actionPaste + + text: qsTr("&Paste") + } + ClideMenu { + title: qsTr("&Edit") + + ClideMenuBarItem { + action: actionUndo + } + ClideMenuBarItem { + action: actionRedo + } + ClideMenuBarItem { + action: actionCut + } + ClideMenuBarItem { + action: actionCopy + } + ClideMenuBarItem { + action: actionPaste + } + } + Action { + id: actionToolWindows + + text: qsTr("&Tool Windows") + } + Action { + id: actionAppearance + + text: qsTr("&Appearance") + } + ClideMenu { + title: qsTr("&View") + + ClideMenuBarItem { + action: actionToolWindows + } + ClideMenuBarItem { + action: actionAppearance + } + } + Action { + id: actionDocumentation + + text: qsTr("&Documentation") + } + Action { + id: actionAbout + + text: qsTr("&About") + } + ClideMenu { + title: qsTr("&Help") + + ClideMenuBarItem { + action: actionDocumentation + } + ClideMenuBarItem { + action: actionAbout + } + } +} diff --git a/qml/main.qml b/qml/main.qml index 041794a..5f4f592 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -5,6 +5,8 @@ import QtQuick.Dialogs import "Menu" +import clide.module 1.0 + ApplicationWindow { id: appWindow @@ -51,7 +53,7 @@ ApplicationWindow { id: navigationView SplitView.fillHeight: true - SplitView.preferredWidth: 250 + SplitView.preferredWidth: 200 color: "#303234" StackLayout { @@ -59,9 +61,9 @@ ApplicationWindow { // Shows the help text. TextArea { - readOnly: true placeholderText: qsTr("File system view placeholder") placeholderTextColor: "white" + readOnly: true wrapMode: TextArea.Wrap } @@ -92,55 +94,134 @@ ApplicationWindow { } } - TextArea { - id: areaText + RowLayout { + // We use a flickable to synchronize the position of the editor and + // the line numbers. This is necessary because the line numbers can + // extend the available height. + Flickable { + id: lineNumbers - color: "#ccced3" - focus: true - persistentSelection: true - selectByMouse: true - // selectedTextColor: control.palette.highlightedText - // selectionColor: control.palette.highlight - textFormat: Qt.AutoText - wrapMode: TextArea.Wrap + Layout.fillHeight: true + Layout.fillWidth: false - background: Rectangle { - color: "#2b2b2b" - implicitHeight: 650 + // Calculate the width based on the logarithmic scale. + Layout.preferredWidth: fontMetrics.averageCharacterWidth * (Math.floor(Math.log10(areaText.lineCount)) + 1) + 10 + contentY: editorFlickable.contentY + interactive: false + visible: true + + Column { + anchors.fill: parent + + Repeater { + id: repeatedLineNumbers + + delegate: Item { + required property int index + + height: Math.ceil(fontMetrics.lineSpacing) + width: parent.width + + Label { + id: numbers + + color: "white" + font: areaText.font + height: parent.height + horizontalAlignment: Text.AlignLeft + text: parent.index + 1 + verticalAlignment: Text.AlignVCenter + width: parent.width + } + Rectangle { + id: indicator + + anchors.left: numbers.right + color: Qt.darker("#FFF", 3) + height: parent.height + width: 1 + } + } + model: LineCount { + id: lineCountModel + + // This count sets the max line numbers shown in the gutter. + count: areaText.lineCount + } + } + } } + Flickable { + id: editorFlickable - onLinkActivated: function (link) { - Qt.openUrlExternally(link); + height: 650 + + property alias areaText: areaText + + Layout.fillHeight: true + Layout.fillWidth: true + boundsBehavior: Flickable.StopAtBounds + + ScrollBar.horizontal: MyScrollBar { + } + ScrollBar.vertical: MyScrollBar { + } + TextArea.flickable: TextArea { + id: areaText + + color: "#ccced3" + focus: true + persistentSelection: true + selectByMouse: true + // selectedTextColor: control.palette.highlightedText + // selectionColor: control.palette.highlight + textFormat: Qt.AutoText + wrapMode: TextArea.Wrap + + background: Rectangle { + color: "#2b2b2b" + } + + onLinkActivated: function (link) { + Qt.openUrlExternally(link); + } + + // TODO: Handle saving + // Component.onCompleted: { + // if (Qt.application.arguments.length === 2) + // textDocument.source = "file:" + Qt.application.arguments[1] + // else + // textDocument.source = "qrc:/texteditor.html" + // } + // textDocument.onStatusChanged: { + // // a message lookup table using computed properties: + // // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer + // const statusMessages = { + // [ TextDocument.ReadError ]: qsTr("Failed to load “%1”"), + // [ TextDocument.WriteError ]: qsTr("Failed to save “%1”"), + // [ TextDocument.NonLocalFileError ]: qsTr("Not a local file: “%1”"), + // } + // const err = statusMessages[textDocument.status] + // if (err) { + // errorDialog.text = err.arg(textDocument.source) + // errorDialog.open() + // } + // } + } + + FontMetrics { + id: fontMetrics + + font: areaText.font + } } - - // TODO: Handle saving - // Component.onCompleted: { - // if (Qt.application.arguments.length === 2) - // textDocument.source = "file:" + Qt.application.arguments[1] - // else - // textDocument.source = "qrc:/texteditor.html" - // } - // textDocument.onStatusChanged: { - // // a message lookup table using computed properties: - // // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer - // const statusMessages = { - // [ TextDocument.ReadError ]: qsTr("Failed to load “%1”"), - // [ TextDocument.WriteError ]: qsTr("Failed to save “%1”"), - // [ TextDocument.NonLocalFileError ]: qsTr("Not a local file: “%1”"), - // } - // const err = statusMessages[textDocument.status] - // if (err) { - // errorDialog.text = err.arg(textDocument.source) - // errorDialog.open() - // } - // } } TextArea { id: areaConsole + height: 100 placeholderText: qsTr("Placeholder for bash terminal.") placeholderTextColor: "white" - height: 100 readOnly: true wrapMode: TextArea.Wrap @@ -153,4 +234,36 @@ ApplicationWindow { } } } + + // We use an inline component to customize the horizontal and vertical + // scroll-bars. This is convenient when the component is only used in one file. + component MyScrollBar: ScrollBar { + id: scrollBar + + background: Rectangle { + color: "#2b2b2b" + implicitHeight: scrollBar.interactive ? 8 : 4 + implicitWidth: scrollBar.interactive ? 8 : 4 + opacity: scrollBar.active && scrollBar.size < 1.0 ? 1.0 : 0.0 + + Behavior on opacity { + OpacityAnimator { + duration: 500 + } + } + } + contentItem: Rectangle { + color: "#4b4f51" + implicitHeight: scrollBar.interactive ? 8 : 4 + implicitWidth: scrollBar.interactive ? 8 : 4 + opacity: scrollBar.active && scrollBar.size < 1.0 ? 1.0 : 0.0 + + Behavior on opacity { + OpacityAnimator { + duration: 1000 + } + } + } + } } + diff --git a/src/main.rs b/src/main.rs index 50edc97..4239053 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,77 +1,103 @@ -// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company -// SPDX-FileContributor: Leon Matthes -// -// SPDX-License-Identifier: MIT OR Apache-2.0 +// TODO: Header #[cxx_qt::bridge] -mod qobject { +pub mod qobject { unsafe extern "C++" { - include!("cxx-qt-lib/qstring.h"); - type QString = cxx_qt_lib::QString; - } - - #[qenum(Greeter)] - pub enum Language { - English, - German, - French, - } - - #[qenum(Greeter)] - pub enum Greeting { - Hello, - Bye, + include!("cxx-qt-lib/qvariant.h"); + type QVariant = cxx_qt_lib::QVariant; + include!(); + type QModelIndex = cxx_qt_lib::QModelIndex; + type QAbstractListModel; } unsafe extern "RustQt" { #[qobject] + #[base = QAbstractListModel] + type AbstractListModel = super::AbstractListModelRust; + + #[qobject] + #[base = AbstractListModel] #[qml_element] - #[qproperty(Greeting, greeting)] - #[qproperty(Language, language)] - type Greeter = super::GreeterRust; + #[qproperty(i32, count)] + type LineCount = super::LineCountRust; + + #[cxx_name = "beginInsertRows"] + #[inherit] + fn beginInsertRows(self: Pin<&mut LineCount>, parent: &QModelIndex, first: i32, last: i32); + + #[cxx_name = "endInsertRows"] + #[inherit] + fn endInsertRows(self: Pin<&mut LineCount>); + + #[cxx_name = "beginRemoveRows"] + #[inherit] + fn beginRemoveRows(self: Pin<&mut LineCount>, parent: &QModelIndex, first: i32, last: i32); + + #[cxx_name = "endRemoveRows"] + #[inherit] + fn endRemoveRows(self: Pin<&mut LineCount>); #[qinvokable] - fn greet(self: &Greeter) -> QString; + pub fn set_line_count(self: Pin<&mut LineCount>, line_count: i32); + + #[qinvokable] + #[cxx_override] + fn data(self: &LineCount, index: &QModelIndex, role: i32) -> QVariant; + + #[qinvokable] + #[cxx_override] + #[cxx_name = "rowCount"] + fn row_count(self: &LineCount, _parent: &QModelIndex) -> i32; } } -use qobject::*; +use cxx_qt::CxxQtType; +use cxx_qt_lib::{QModelIndex, QVariant}; -impl Greeting { - fn translate(&self, language: Language) -> String { - match (self, language) { - (&Greeting::Hello, Language::English) => "Hello, World!", - (&Greeting::Hello, Language::German) => "Hallo, Welt!", - (&Greeting::Hello, Language::French) => "Bonjour, le monde!", - (&Greeting::Bye, Language::English) => "Bye!", - (&Greeting::Bye, Language::German) => "Auf Wiedersehen!", - (&Greeting::Bye, Language::French) => "Au revoir!", - _ => "🤯", +impl qobject::LineCount { + pub fn set_line_count(mut self: std::pin::Pin<&mut Self>, line_count: i32) { + let current_count = self.as_mut().rust_mut().count; + if line_count < 0 || current_count == line_count { + log::warn!( + "Can't set line count: {}; Current count: {}", + line_count, + current_count + ); + return; } - .to_string() - } -} - -pub struct GreeterRust { - greeting: Greeting, - language: Language, -} - -impl Default for GreeterRust { - fn default() -> Self { - Self { - greeting: Greeting::Hello, - language: Language::English, + if current_count < line_count { + self.as_mut() + .beginInsertRows(&QModelIndex::default(), current_count, line_count - 1); + self.as_mut().endInsertRows(); + } else if current_count > line_count { + self.as_mut() + .beginRemoveRows(&QModelIndex::default(), line_count, current_count - 1); + self.as_mut().endRemoveRows(); } + self.as_mut().rust_mut().count = line_count; + log::warn!( + "Line count changed from {} to {}", + current_count, + line_count + ); + } + + pub fn row_count(self: &Self, _parent: &QModelIndex) -> i32 { + *self.count() + } + + pub fn data(self: &Self, _index: &QModelIndex, _role: i32) -> QVariant { + QVariant::default() } } -use cxx_qt_lib::QString; +/// A struct which inherits from QAbstractListModel +#[derive(Default)] +pub struct AbstractListModelRust {} -impl qobject::Greeter { - fn greet(&self) -> QString { - QString::from(self.greeting.translate(self.language)) - } +#[derive(Default)] +pub struct LineCountRust { + pub count: i32, } fn main() {