diff --git a/qml/ClideAboutWindow.qml b/qml/ClideAboutWindow.qml index 6d02ec0..be5f034 100644 --- a/qml/ClideAboutWindow.qml +++ b/qml/ClideAboutWindow.qml @@ -9,11 +9,12 @@ import clide.module 1.0 ApplicationWindow { id: root - width: 450 - height: 350 + + color: RustColors.gutter // Create the window with no frame and keep it on top. flags: Qt.Window | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint - color: RustColors.gutter + height: 350 + width: 450 // Hide the window when it loses focus. onActiveChanged: { @@ -27,48 +28,38 @@ ApplicationWindow { id: logo anchors.left: parent.left + anchors.margins: 20 anchors.right: parent.right anchors.top: parent.top - anchors.margins: 20 - - source: "qrc:/images/kilroy.png" - sourceSize.width: 80 - sourceSize.height: 80 - fillMode: Image.PreserveAspectFit - - smooth: true antialiasing: true asynchronous: true + fillMode: Image.PreserveAspectFit + smooth: true + source: "qrc:/images/kilroy.png" + sourceSize.height: 80 + sourceSize.width: 80 } - ScrollView { - anchors.top: logo.bottom - anchors.left: parent.left - anchors.right: parent.right anchors.bottom: parent.bottom + anchors.left: parent.left anchors.margins: 20 + anchors.right: parent.right + anchors.top: logo.bottom TextArea { - selectedTextColor: RustColors.editor_highlighted_text - selectionColor: RustColors.editor_highlight - horizontalAlignment: Text.AlignHCenter - textFormat: Text.RichText - - text: qsTr("
A simple text editor written in Rust and QML using CXX-Qt.
" - + "Personal website shaunreed.com
" - + "Project notes knoats.com
" - + "This project is developed at git.shaunreed.com
" - + "" - + "Copyright (C) 2026 Shaun Reed, all rights reserved.
") - color: RustColors.editor_text - wrapMode: Text.WordWrap - readOnly: true antialiasing: true background: null + color: RustColors.editor_text + horizontalAlignment: Text.AlignHCenter + readOnly: true + selectedTextColor: RustColors.editor_highlighted_text + selectionColor: RustColors.editor_highlight + text: qsTr("A simple text editor written in Rust and QML using CXX-Qt.
" + "Personal website shaunreed.com
" + "Project notes knoats.com
" + "This project is developed at git.shaunreed.com
" + "" + "Copyright (C) 2026 Shaun Reed, all rights reserved.
") + textFormat: Text.RichText + wrapMode: Text.WordWrap onLinkActivated: function (link) { - Qt.openUrlExternally(link) + Qt.openUrlExternally(link); } } } diff --git a/qml/ClideEditor.qml b/qml/ClideEditor.qml index a7fd88f..bb64b26 100644 --- a/qml/ClideEditor.qml +++ b/qml/ClideEditor.qml @@ -11,15 +11,16 @@ import Logger 1.0 SplitView { id: root - Layout.fillHeight: true - Layout.fillWidth: true - orientation: Qt.Vertical // The path to the file to show in the text editor. // This is updated by a signal caught within ClideProjectView. // Initialized by the Default trait for the Rust QML singleton FileSystem. required property string filePath + Layout.fillHeight: true + Layout.fillWidth: true + orientation: Qt.Vertical + // Customized handle to drag between the Editor and the Console. handle: Rectangle { border.color: SplitHandle.pressed ? RustColors.pressed : SplitHandle.hovered ? RustColors.hovered : RustColors.gutter @@ -34,12 +35,23 @@ SplitView { } } } + + Component.onCompleted: { + // Show logging is working. + Logger.info("Info logs"); + Logger.warn("Warning logs"); + Logger.debug("Debug logs"); + Logger.error("Error logs"); + Logger.trace("Trace logs"); + } + 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 + Layout.fillHeight: true Layout.fillWidth: false // Calculating the width correctly is important as the number grows. @@ -58,20 +70,22 @@ SplitView { Repeater { id: repeatedLineNumbers + // TODO: Bug where text wrapping shows as new line number. model: textArea.lineCount // This Item is used for each line number in the gutter. delegate: Item { + required property int index + // Calculates the height of each line in the text area. height: textArea.contentHeight / textArea.lineCount width: parent.width - required property int index - // Show the line number. Label { id: numbers + color: RustColors.linenumber font: textArea.font height: parent.height @@ -79,6 +93,7 @@ SplitView { text: parent.index + 1 verticalAlignment: Text.AlignVCenter width: parent.width - indicator.width + background: Rectangle { color: RustColors.terminal_background } @@ -86,6 +101,7 @@ SplitView { // Draw edge along the right side of the line number. Rectangle { id: indicator + anchors.left: numbers.right color: RustColors.linenumber height: parent.height @@ -97,6 +113,7 @@ SplitView { } Flickable { id: editorFlickable + Layout.fillHeight: true Layout.fillWidth: true boundsBehavior: Flickable.StopAtBounds @@ -106,18 +123,18 @@ SplitView { } ScrollBar.vertical: MyScrollBar { } - TextArea.flickable: TextArea { id: textArea + + antialiasing: true focus: true persistentSelection: true - antialiasing: true selectByMouse: true - selectionColor: RustColors.editor_highlight selectedTextColor: RustColors.editor_highlighted_text + selectionColor: RustColors.editor_highlight + text: FileSystem.readFile(root.filePath) textFormat: Qt.AutoText wrapMode: TextArea.Wrap - text: FileSystem.readFile(root.filePath) onLinkActivated: function (link) { Qt.openUrlExternally(link); @@ -155,6 +172,7 @@ SplitView { } ClideLogger { id: areaConsole + } // We use an inline component to customize the horizontal and vertical @@ -164,12 +182,13 @@ SplitView { // Scroll bar gutter background: Rectangle { + color: RustColors.scrollbar_gutter implicitHeight: scrollBar.interactive ? 8 : 4 implicitWidth: scrollBar.interactive ? 8 : 4 - color: RustColors.scrollbar_gutter // Fade the scrollbar gutter when inactive. opacity: scrollBar.active && scrollBar.size < 1.0 ? 1.0 : 0.2 + Behavior on opacity { OpacityAnimator { duration: 500 @@ -179,20 +198,21 @@ SplitView { // Scroll bar contentItem: Rectangle { - implicitHeight: scrollBar.interactive ? 8 : 4 - implicitWidth: scrollBar.interactive ? 8 : 4 // If we don't need a scrollbar, fallback to the gutter color. // If the scrollbar is required change it's color based on activity. color: scrollBar.size < 1.0 ? scrollBar.active ? RustColors.scrollbar_active : RustColors.scrollbar : RustColors.scrollbar_gutter + implicitHeight: scrollBar.interactive ? 8 : 4 + implicitWidth: scrollBar.interactive ? 8 : 4 + // Fade the scrollbar when inactive. + opacity: scrollBar.active && scrollBar.size < 1.0 ? 1.0 : 0.35 + // Smooth transition between color changes based on the state above. Behavior on color { ColorAnimation { duration: 1000 } } - // Fade the scrollbar when inactive. - opacity: scrollBar.active && scrollBar.size < 1.0 ? 1.0 : 0.35 Behavior on opacity { OpacityAnimator { duration: 500 @@ -200,14 +220,4 @@ SplitView { } } } - - - Component.onCompleted: { - // Show logging is working. - Logger.info("Info logs") - Logger.warn("Warning logs") - Logger.debug("Debug logs") - Logger.error("Error logs") - Logger.trace("Trace logs") - } } diff --git a/qml/ClideLogger.qml b/qml/ClideLogger.qml index 827626b..4a79eb2 100644 --- a/qml/ClideLogger.qml +++ b/qml/ClideLogger.qml @@ -9,47 +9,52 @@ import clide.module 1.0 import Logger 1.0 Item { - ListModel { id: model } + ListModel { + id: model + } Rectangle { anchors.fill: parent color: "#111" } - ListView { id: listView - anchors.fill: parent - model: model - clip: true function getLogColor(level) { switch (level) { - case "INFO": - return RustColors.info_log - case "DEBUG": - return RustColors.debug_log - case "WARN": - return RustColors.warn_log - case "ERROR": - return RustColors.error_log - case "TRACE": - return RustColors.trace_log - default: - return RustColors.info_log + case "INFO": + return RustColors.info_log; + case "DEBUG": + return RustColors.debug_log; + case "WARN": + return RustColors.warn_log; + case "ERROR": + return RustColors.error_log; + case "TRACE": + return RustColors.trace_log; + default: + return RustColors.info_log; } } + anchors.fill: parent + clip: true + model: model + delegate: Text { - text: `[${level}] ${message}` - font.family: "monospace" color: listView.getLogColor(level) + font.family: "monospace" + text: `[${level}] ${message}` } } - Connections { - target: Logger function onLogged(level, message) { - model.append({ level, message }) + model.append({ + level, + message + }); } + + target: Logger } } diff --git a/qml/ClideMenuBar.qml b/qml/ClideMenuBar.qml index 0675623..9cd685b 100644 --- a/qml/ClideMenuBar.qml +++ b/qml/ClideMenuBar.qml @@ -10,32 +10,8 @@ import clide.module 1.0 MenuBar { // Background for this MenuBar. background: Rectangle { - color: RustColors.menubar border.color: RustColors.explorer_background - } - - // Base settings for each Menu. - component ClideMenu : Menu { - background: Rectangle { - color: RustColors.explorer_background - implicitWidth: 100 - radius: 2 - } - } - - // Base settings for each MenuItem. - component ClideMenuItem : MenuItem { - id: root - - background: Rectangle { - color: root.hovered ? RustColors.hovered : RustColors.unhovered - radius: 1.0 - } - contentItem: IconLabel { - color: "black" - font.family: "Helvetica" - text: root.text - } + color: RustColors.menubar } // @@ -70,6 +46,7 @@ MenuBar { } ClideMenuItem { action: actionOpen + onTriggered: FileSystem.setDirectory(FileSystem.filePath) } ClideMenuItem { @@ -162,8 +139,8 @@ MenuBar { // Help Menu ClideAboutWindow { id: clideAbout - } + } Action { id: actionDocumentation @@ -171,10 +148,11 @@ MenuBar { } Action { id: actionAbout - // Toggle the about window with the menu item is clicked. - onTriggered: clideAbout.visible = !clideAbout.visible text: qsTr("&About") + + // Toggle the about window with the menu item is clicked. + onTriggered: clideAbout.visible = !clideAbout.visible } ClideMenu { title: qsTr("&Help") @@ -186,4 +164,28 @@ MenuBar { action: actionAbout } } + + // Base settings for each Menu. + component ClideMenu: Menu { + background: Rectangle { + color: RustColors.explorer_background + implicitWidth: 100 + radius: 2 + } + } + + // Base settings for each MenuItem. + component ClideMenuItem: MenuItem { + id: root + + background: Rectangle { + color: root.hovered ? RustColors.hovered : RustColors.unhovered + radius: 1.0 + } + contentItem: IconLabel { + color: "black" + font.family: "Helvetica" + text: root.text + } + } } diff --git a/qml/ClideProjectView.qml b/qml/ClideProjectView.qml index 8948706..309404b 100644 --- a/qml/ClideProjectView.qml +++ b/qml/ClideProjectView.qml @@ -22,6 +22,7 @@ SplitView { // Customized handle to drag between the Navigation and the Editor. handle: Rectangle { id: verticalSplitHandle + border.color: SplitHandle.pressed ? RustColors.pressed : SplitHandle.hovered ? RustColors.hovered : RustColors.gutter color: SplitHandle.pressed ? RustColors.pressed : SplitHandle.hovered ? RustColors.hovered : RustColors.gutter implicitWidth: 8 @@ -37,65 +38,71 @@ SplitView { Rectangle { id: navigationView - color: RustColors.explorer_background SplitView.fillHeight: true + SplitView.maximumWidth: 250 SplitView.minimumWidth: 0 SplitView.preferredWidth: 200 - SplitView.maximumWidth: 250 + color: RustColors.explorer_background ColumnLayout { spacing: 2 + // TODO: Make a ClideBreadCrumb element to support select parent paths as root Rectangle { - width: navigationView.width - height: 25 color: RustColors.explorer_background + height: 25 + width: navigationView.width + Text { id: breadCrumb + anchors.fill: parent - text: clideTreeView.rootDirectory color: RustColors.explorer_text elide: Text.ElideLeft horizontalAlignment: Text.AlignHCenter + text: clideTreeView.rootDirectory verticalAlignment: Text.AlignVCenter } - TapHandler { acceptedButtons: Qt.RightButton + onSingleTapped: (eventPoint, button) => contextMenu.popup() } - Menu { id: contextMenu + Action { text: qsTr("Reset root index") + onTriggered: { - Logger.log("Resetting root directory: " + clideTreeView.originalRootDirectory) - clideTreeView.rootDirectory = clideTreeView.originalRootDirectory + Logger.log("Resetting root directory: " + clideTreeView.originalRootDirectory); + clideTreeView.rootDirectory = clideTreeView.originalRootDirectory; } } } } - ClideTreeView { id: clideTreeView - onFileClicked: path => clideEditor.filePath = path - width: navigationView.width + height: navigationView.height // Path to the directory opened in the file explorer. originalRootDirectory: root.projectDir rootDirectory: root.projectDir + width: navigationView.width + + onFileClicked: path => clideEditor.filePath = path onRootDirectoryChanged: { - Logger.log("Setting root directory: " + clideTreeView.rootDirectory) - breadCrumb.text = clideTreeView.rootDirectory + Logger.log("Setting root directory: " + clideTreeView.rootDirectory); + breadCrumb.text = clideTreeView.rootDirectory; } } } } ClideEditor { id: clideEditor + SplitView.fillWidth: true // Provide a path to the file currently open in the text editor. diff --git a/qml/ClideTreeView.qml b/qml/ClideTreeView.qml index 629d70c..8847df5 100644 --- a/qml/ClideTreeView.qml +++ b/qml/ClideTreeView.qml @@ -11,141 +11,19 @@ import Logger 1.0 TreeView { id: fileSystemTreeView - model: FileSystem property int lastIndex: -1 - required property string originalRootDirectory property string rootDirectory signal fileClicked(string filePath) - rootIndex: FileSystem.setDirectory(fileSystemTreeView.rootDirectory) - leftMargin: 5 boundsBehavior: Flickable.StopAtBounds boundsMovement: Flickable.StopAtBounds clip: true - - // The delegate represents a single entry in the filesystem. - delegate: TreeViewDelegate { - id: treeDelegate - indentation: 12 - implicitWidth: fileSystemTreeView.width > 0 ? fileSystemTreeView.width : 250 - implicitHeight: 25 - - required property int index - required property url filePath - required property string fileName - - indicator: Image { - id: directoryIcon - - function setSourceImage() { - let folderOpen = "data:image/svg+xml;utf8,"; - let folderClosed = "data:image/svg+xml;utf8,"; - let file = "data:image/svg+xml;utf8,"; - // If the item has children, it's a directory. - if (treeDelegate.hasChildren) { - return treeDelegate.expanded ? - folderOpen : folderClosed; - } - return file - } - - x: treeDelegate.leftMargin + (treeDelegate.depth * treeDelegate.indentation) - anchors.verticalCenter: parent.verticalCenter - source: setSourceImage() - sourceSize.width: 15 - sourceSize.height: 15 - fillMode: Image.PreserveAspectFit - - smooth: true - antialiasing: true - asynchronous: true - } - - contentItem: Text { - text: treeDelegate.fileName - color: RustColors.explorer_text - } - - background: Rectangle { - // TODO: Fix flickering from color transition on states here. - color: (treeDelegate.index === fileSystemTreeView.lastIndex) - ? RustColors.explorer_text_selected - : (hoverHandler.hovered ? RustColors.explorer_hovered : "transparent") - radius: 2.5 - opacity: hoverHandler.hovered ? 0.75 : 1.0 - - Behavior on color { - ColorAnimation { - duration: 300 - } - } - } - - MultiEffect { - id: iconOverlay - - anchors.fill: directoryIcon - source: directoryIcon - colorization: 1.0 - brightness: 1.0 - colorizationColor: { - const isFile = !treeDelegate.hasChildren; - if (isFile) - return Qt.lighter(RustColors.explorer_folder, 2) - - const isExpandedFolder = treeDelegate.expanded && treeDelegate.hasChildren; - if (isExpandedFolder) - return Qt.darker(RustColors.explorer_folder, 2) - else - return RustColors.explorer_folder - } - } - - HoverHandler { - id: hoverHandler - acceptedDevices: PointerDevice.Mouse - } - - TapHandler { - acceptedButtons: Qt.LeftButton | Qt.RightButton - onSingleTapped: (eventPoint, button) => { - switch (button) { - case Qt.LeftButton: - fileSystemTreeView.toggleExpanded(treeDelegate.row) - // If this model item doesn't have children, it means it's - // representing a file. - if (!treeDelegate.hasChildren) - fileSystemTreeView.fileClicked(treeDelegate.filePath) - break; - case Qt.RightButton: - contextMenu.popup(); - break; - } - } - } - - Menu { - id: contextMenu - Action { - text: qsTr("Set as root index") - enabled: treeDelegate.hasChildren - onTriggered: { - Logger.debug("Setting new root directory: " + treeDelegate.filePath) - fileSystemTreeView.rootDirectory = treeDelegate.filePath - } - } - Action { - text: qsTr("Reset root index") - onTriggered: { - Logger.log("Resetting root directory: " + fileSystemTreeView.originalRootDirectory) - fileSystemTreeView.rootDirectory = fileSystemTreeView.originalRootDirectory - } - } - } - } + leftMargin: 5 + model: FileSystem + rootIndex: FileSystem.setDirectory(fileSystemTreeView.rootDirectory) // Provide our own custom ScrollIndicator for the TreeView. ScrollIndicator.vertical: ScrollIndicator { @@ -153,10 +31,9 @@ TreeView { implicitWidth: 15 contentItem: Rectangle { - implicitWidth: 6 - implicitHeight: 6 - color: RustColors.scrollbar + implicitHeight: 6 + implicitWidth: 6 opacity: fileSystemTreeView.movingVertically ? 0.5 : 0.0 Behavior on opacity { @@ -166,4 +43,122 @@ TreeView { } } } + + // The delegate represents a single entry in the filesystem. + delegate: TreeViewDelegate { + id: treeDelegate + + required property string fileName + required property url filePath + required property int index + + implicitHeight: 25 + implicitWidth: fileSystemTreeView.width > 0 ? fileSystemTreeView.width : 250 + indentation: 12 + + background: Rectangle { + // TODO: Fix flickering from color transition on states here. + color: (treeDelegate.index === fileSystemTreeView.lastIndex) ? RustColors.explorer_text_selected : (hoverHandler.hovered ? RustColors.explorer_hovered : "transparent") + opacity: hoverHandler.hovered ? 0.75 : 1.0 + radius: 2.5 + + Behavior on color { + ColorAnimation { + duration: 300 + } + } + } + contentItem: Text { + color: RustColors.explorer_text + text: treeDelegate.fileName + } + indicator: Image { + id: directoryIcon + + function setSourceImage() { + let folderOpen = "data:image/svg+xml;utf8,"; + let folderClosed = "data:image/svg+xml;utf8,"; + let file = "data:image/svg+xml;utf8,"; + // If the item has children, it's a directory. + if (treeDelegate.hasChildren) { + return treeDelegate.expanded ? folderOpen : folderClosed; + } + return file; + } + + anchors.verticalCenter: parent.verticalCenter + antialiasing: true + asynchronous: true + fillMode: Image.PreserveAspectFit + smooth: true + source: setSourceImage() + sourceSize.height: 15 + sourceSize.width: 15 + x: treeDelegate.leftMargin + (treeDelegate.depth * treeDelegate.indentation) + } + + MultiEffect { + id: iconOverlay + + anchors.fill: directoryIcon + brightness: 1.0 + colorization: 1.0 + colorizationColor: { + const isFile = !treeDelegate.hasChildren; + if (isFile) + return Qt.lighter(RustColors.explorer_folder, 2); + + const isExpandedFolder = treeDelegate.expanded && treeDelegate.hasChildren; + if (isExpandedFolder) + return Qt.darker(RustColors.explorer_folder, 2); + else + return RustColors.explorer_folder; + } + source: directoryIcon + } + HoverHandler { + id: hoverHandler + + acceptedDevices: PointerDevice.Mouse + } + TapHandler { + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onSingleTapped: (eventPoint, button) => { + switch (button) { + case Qt.LeftButton: + fileSystemTreeView.toggleExpanded(treeDelegate.row); + // If this model item doesn't have children, it means it's + // representing a file. + if (!treeDelegate.hasChildren) + fileSystemTreeView.fileClicked(treeDelegate.filePath); + break; + case Qt.RightButton: + contextMenu.popup(); + break; + } + } + } + Menu { + id: contextMenu + + Action { + enabled: treeDelegate.hasChildren + text: qsTr("Set as root index") + + onTriggered: { + Logger.debug("Setting new root directory: " + treeDelegate.filePath); + fileSystemTreeView.rootDirectory = treeDelegate.filePath; + } + } + Action { + text: qsTr("Reset root index") + + onTriggered: { + Logger.log("Resetting root directory: " + fileSystemTreeView.originalRootDirectory); + fileSystemTreeView.rootDirectory = fileSystemTreeView.originalRootDirectory; + } + } + } + } } diff --git a/qml/Logger/Logger.qml b/qml/Logger/Logger.qml index 4754300..7e60c04 100644 --- a/qml/Logger/Logger.qml +++ b/qml/Logger/Logger.qml @@ -8,33 +8,28 @@ import QtQuick QtObject { signal logged(string level, string message) - function log(msg) { - console.log(msg) - logged("INFO", msg) - } - - function info(msg) { - console.log(msg) - logged("INFO", msg) - } - function debug(msg) { - console.log(msg) - logged("DEBUG", msg) + console.log(msg); + logged("DEBUG", msg); } - - function warn(msg) { - console.warn(msg) - logged("WARN", msg) - } - function error(msg) { - console.error(msg) - logged("ERROR", msg) + console.error(msg); + logged("ERROR", msg); + } + function info(msg) { + console.log(msg); + logged("INFO", msg); + } + function log(msg) { + console.log(msg); + logged("INFO", msg); } - function trace(msg) { - console.log(msg) - logged("TRACE", msg) + console.log(msg); + logged("TRACE", msg); + } + function warn(msg) { + console.warn(msg); + logged("WARN", msg); } } diff --git a/qml/main.qml b/qml/main.qml index 6f71d32..d4facbc 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -12,14 +12,15 @@ import clide.module 1.0 ApplicationWindow { id: appWindow + required property string appContextPath + height: 800 title: "CLIDE" visible: true width: 1200 - required property string appContextPath - - menuBar: ClideMenuBar { } + menuBar: ClideMenuBar { + } MessageDialog { id: errorDialog @@ -28,6 +29,7 @@ ApplicationWindow { } ClideProjectView { id: clideProjectView + projectDir: appWindow.appContextPath } }