20 Commits

Author SHA1 Message Date
67bf82d0cb Add qmllint.ini 2026-02-06 18:59:38 -05:00
39377b32f0 WIP 2026-02-06 18:59:02 -05:00
176efb97b7 Clean up ClideScrollBar. 2026-02-04 19:52:05 -05:00
755066d847 Clean up ClideHandle. 2026-02-04 19:29:52 -05:00
773d7818b5 Auto scroll logger. 2026-02-04 18:35:43 -05:00
7e58e3ee03 Split basic components for reuse. 2026-02-02 23:05:53 -05:00
0f50577d78 Fix scrollbar animations. 2026-02-02 18:21:28 -05:00
29024e3999 Format with qmlformat. 2026-02-02 18:08:37 -05:00
5af09485a3 Add trace logs. 2026-02-02 18:01:53 -05:00
e5b91eaed8 Add basic debug logger. 2026-02-01 20:23:23 -05:00
be383869b2 Add context menu on breadcrumbs.
The only option is to reset the root directory.
2026-02-01 19:10:15 -05:00
b9eee50e52 Fix menu bar colors. 2026-02-01 18:42:21 -05:00
4cc43916cb Update breadcrumbs when root directory changes. 2026-02-01 18:23:44 -05:00
a5bed9ed2c Fix panic when loading bad text in the GUI. 2026-02-01 17:15:21 -05:00
0fac2b71ab Fix TreeView nesting for ColumnLayout. 2026-02-01 15:35:40 -05:00
db2f878018 Fix about page image loading. 2026-02-01 13:44:43 -05:00
048d40eb83 Fix TreeView root index.
Parent folders will now be hidden based on the root folder selection in
the GUI.
2026-02-01 13:07:27 -05:00
c70bba16e4 Revert "Try to use QSortFilterProxyModel via cxx-qt."
This reverts commit 325cf285fc.
2026-02-01 13:07:10 -05:00
325cf285fc Try to use QSortFilterProxyModel via cxx-qt.
This may be easier if I just wrap a QAbstractItemModel in C++ and use
cxx-qt to pull it into QML? That way I could do C++ pointer things in
C++ and rust things in rust.
2026-01-31 21:47:19 -05:00
aa8590cd5c Add license headers. 2026-01-31 08:02:16 -05:00
30 changed files with 469 additions and 638 deletions

View File

@@ -1,6 +1,3 @@
[build] [build]
rustflags = [ "-C", "link-arg=-fuse-ld=lld", ] rustflags = [ "-C", "link-arg=-fuse-ld=lld", ]
[env]
QMAKE="/opt/Qt/6.7.3/gcc_64/bin/qmake6"
LD_LIBRARY_PATH="/opt/Qt/6.7.3/gcc_64/lib"

10
Cargo.lock generated
View File

@@ -295,7 +295,6 @@ dependencies = [
"cxx-qt", "cxx-qt",
"cxx-qt-build", "cxx-qt-build",
"cxx-qt-lib", "cxx-qt-lib",
"devicons",
"dirs", "dirs",
"edtui", "edtui",
"log", "log",
@@ -668,15 +667,6 @@ dependencies = [
"syn 2.0.114", "syn 2.0.114",
] ]
[[package]]
name = "devicons"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830e47e2f330cf4fdd5a958dcef921b9523ffc21ab6713aa5e77ba2cce03904b"
dependencies = [
"lazy_static",
]
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"

View File

@@ -18,7 +18,6 @@ tui-logger = "0.18.1"
edtui = "0.11.1" edtui = "0.11.1"
strum = "0.27.2" strum = "0.27.2"
uuid = { version = "1.19.0", features = ["v4"] } uuid = { version = "1.19.0", features = ["v4"] }
devicons = "0.6.12"
[build-dependencies] [build-dependencies]
# The link_qt_object_files feature is required for statically linking Qt 6. # The link_qt_object_files feature is required for statically linking Qt 6.

View File

@@ -19,14 +19,13 @@ And of course, [Rust](https://www.rust-lang.org/tools/install).
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
``` ```
This project requires at least Qt 6.7.3 To check your Qt version This project requires at least Qt 6.7. To check your Qt version
```bash ```bash
qmake6 -query QT_VERSION qmake6 -query QT_VERSION
``` ```
Use the [Qt Installer](https://www.qt.io/development/download) to download and install the Qt version of your choice. Use the [Qt Installer](https://www.qt.io/development/download) to download and install the Qt version of your choice.
If the installer is run with `sudo`, the default install location is `/opt/Qt`, otherwise Qt will be installed into your home directory.
**You must set the QMAKE variable before building clide**. This should be a path to `qmake6` binary installed on your system. **You must set the QMAKE variable before building clide**. This should be a path to `qmake6` binary installed on your system.
The following export is the default installation path for Qt 6.7 on Ubuntu 24.04 The following export is the default installation path for Qt 6.7 on Ubuntu 24.04
@@ -36,14 +35,6 @@ export QMAKE=$HOME/Qt/6.7.3/gcc_64/bin/qmake6
export LD_LIBRARY_PATH=$HOME/Qt/6.7.3/gcc_64/lib export LD_LIBRARY_PATH=$HOME/Qt/6.7.3/gcc_64/lib
``` ```
Though environment variables set using `export` will take precedence, these can also be set in [.cargo/config.toml](./.cargo/config.toml) for conveinence
```toml
[env]
QMAKE="/opt/Qt/6.7.3/gcc_64/bin/qmake6"
LD_LIBRARY_PATH="/opt/Qt/6.7.3/gcc_64/lib"
```
## Usage ## Usage
To install and run clide To install and run clide
@@ -107,8 +98,6 @@ cargo run
clide clide
``` ```
![image](./resources/gui.png)
## Development ## Development
It's recommended to use RustRover or Qt Creator for development. It's recommended to use RustRover or Qt Creator for development.

View File

@@ -2,21 +2,16 @@ use cxx_qt_build::{CxxQtBuilder, QmlModule};
fn main() { fn main() {
CxxQtBuilder::new_qml_module(QmlModule::new("clide.module").qml_files(&[ CxxQtBuilder::new_qml_module(QmlModule::new("clide.module").qml_files(&[
"qml/ClideApplicationView.qml",
"qml/ClideEditorView.qml",
"qml/ClideExplorerView.qml",
"qml/ClideTreeView.qml",
"qml/Components/ClideAboutWindow.qml",
"qml/Components/ClideBreadCrumbs.qml",
"qml/Components/ClideEditor.qml",
"qml/Components/ClideHandle.qml",
"qml/Components/ClideLogger.qml",
"qml/Components/ClideMenu.qml",
"qml/Components/ClideMenuBar.qml",
"qml/Components/ClideMenuItem.qml",
"qml/Components/ClideScrollBar.qml",
"qml/Logger/Logger.qml",
"qml/main.qml", "qml/main.qml",
"qml/ClideAboutWindow.qml",
"qml/ClideTreeView.qml",
"qml/ClideProjectView.qml",
"qml/ClideEditor.qml",
"qml/ClideMenuBar.qml",
"qml/ClideLogger.qml",
"qml/Components/ClideScrollBar.qml",
"qml/Components/ClideHandle.qml",
"qml/Logger/Logger.qml",
])) ]))
// Link Qt's Network library // Link Qt's Network library
// - Qt Core is always linked // - Qt Core is always linked

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -1,46 +0,0 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import clide.module 1.0
import Logger 1.0
SplitView {
id: root
// Path to the directory of the project opened in clide.
required property string projectDir
anchors.fill: parent
// Customized handle to drag between the Navigation and the Editor.
handle: ClideHandle {
hovered: SplitHandle.hovered
pressed: SplitHandle.pressed
}
ClideExplorerView {
SplitView.fillHeight: true
SplitView.preferredWidth: 200
projectDir: root.projectDir
// Open files when clicked in the explorer.
onFileClicked: path => {
Logger.trace("Setting editor path from ClideExplorerView signal: " + path)
clideEditorView.filePath = path;
}
}
ClideEditorView {
id: clideEditorView
SplitView.fillHeight: true
SplitView.fillWidth: true
// Provide a path to the file currently open in the text editor.
// Initialized using the Default trait in Rust QML singleton FileSystem.
filePath: FileSystem.filePath
}
}

View File

@@ -9,12 +9,34 @@ import QtQuick.Layouts
import clide.module 1.0 import clide.module 1.0
import Logger 1.0 import Logger 1.0
Rectangle { SplitView {
color: RustColors.editor_background id: root
// 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: ClideHandle {
pressed: SplitHandle.pressed
hovered: SplitHandle.hovered
}
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 { RowLayout {
anchors.fill: parent
// We use a flickable to synchronize the position of the editor and // We use a flickable to synchronize the position of the editor and
// the line numbers. This is necessary because the line numbers can // the line numbers. This is necessary because the line numbers can
// extend the available height. // extend the available height.
@@ -62,8 +84,12 @@ Rectangle {
text: parent.index + 1 text: parent.index + 1
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
width: parent.width - indicator.width width: parent.width - indicator.width
background: Rectangle {
color: RustColors.terminal_background
} }
// Draw an edge along the right side of the line number. }
// Draw edge along the right side of the line number.
Rectangle { Rectangle {
id: indicator id: indicator
@@ -104,6 +130,28 @@ Rectangle {
onLinkActivated: function (link) { onLinkActivated: function (link) {
Qt.openUrlExternally(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 { FontMetrics {
@@ -113,4 +161,8 @@ Rectangle {
} }
} }
} }
ClideLogger {
id: areaConsole
}
} }

View File

@@ -1,49 +0,0 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import clide.module 1.0
import Logger 1.0
Rectangle {
id: root
// The path to the file to show in the text editor.
// This is updated by a signal caught within ClideApplicationView.
required property string filePath
clip: true
color: "transparent"
radius: 20
SplitView {
anchors.fill: parent
orientation: Qt.Vertical
spacing: 3
// Customized handle to drag between the Editor and the Console.
handle: ClideHandle {
hovered: SplitHandle.hovered
pressed: SplitHandle.pressed
}
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");
}
ClideEditor {
SplitView.preferredHeight: 650
}
ClideLogger {
}
}
}

View File

@@ -1,62 +0,0 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import clide.module 1.0
import Logger 1.0
Rectangle {
id: root
required property string projectDir
signal fileClicked(string path)
clip: true
color: RustColors.explorer_background
topLeftRadius: 10
ColumnLayout {
anchors.fill: parent
spacing: 5
ClideBreadCrumbs {
id: breadCrumb
Layout.fillWidth: true
Layout.leftMargin: 15
Layout.rightMargin: 15
Layout.topMargin: 10
path: clideTreeView.rootDirectory
onCrumbClicked: path => {
Logger.trace("Crumb clicked: " + path);
clideTreeView.rootDirectory = path;
}
onResetRoot: {
clideTreeView.rootDirectory = clideTreeView.originalRootDirectory;
}
}
ClideTreeView {
id: clideTreeView
Layout.fillHeight: true
Layout.fillWidth: true
// Path to the directory opened in the file explorer.
originalRootDirectory: root.projectDir
rootDirectory: root.projectDir
// Pass the signal to the parent component using another signal.
onFileClicked: path => root.fileClicked(path)
onRootDirectoryChanged: {
Logger.log("Setting root directory: " + clideTreeView.rootDirectory);
breadCrumb.path = clideTreeView.rootDirectory;
}
}
}
}

View File

@@ -8,14 +8,15 @@ import QtQuick.Controls
import clide.module 1.0 import clide.module 1.0
import Logger 1.0 import Logger 1.0
Rectangle { Item {
color: RustColors.terminal_background
radius: 10
ListModel { ListModel {
id: model id: model
} }
Rectangle {
anchors.fill: parent
color: "#111"
}
ListView { ListView {
id: listView id: listView
@@ -37,6 +38,7 @@ Rectangle {
} }
anchors.fill: parent anchors.fill: parent
clip: true
model: model model: model
delegate: Text { delegate: Text {

191
qml/ClideMenuBar.qml Normal file
View File

@@ -0,0 +1,191 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
import QtQuick
import QtQuick.Controls
import clide.module 1.0
MenuBar {
// Background for this MenuBar.
background: Rectangle {
border.color: RustColors.explorer_background
color: RustColors.menubar
}
//
// File Menu
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")
ClideMenuItem {
action: actionNewProject
}
ClideMenuItem {
action: actionOpen
onTriggered: FileSystem.setDirectory(FileSystem.filePath)
}
ClideMenuItem {
action: actionSave
}
MenuSeparator {
background: Rectangle {
border.color: color
color: RustColors.explorer_background
implicitHeight: 3
implicitWidth: 200
}
}
ClideMenuItem {
action: actionExit
}
}
//
// Edit Menu
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")
ClideMenuItem {
action: actionUndo
}
ClideMenuItem {
action: actionRedo
}
ClideMenuItem {
action: actionCut
}
ClideMenuItem {
action: actionCopy
}
ClideMenuItem {
action: actionPaste
}
}
//
// View Menu
Action {
id: actionToolWindows
text: qsTr("&Tool Windows")
}
Action {
id: actionAppearance
text: qsTr("&Appearance")
}
ClideMenu {
title: qsTr("&View")
ClideMenuItem {
action: actionToolWindows
}
ClideMenuItem {
action: actionAppearance
}
}
//
// Help Menu
ClideAboutWindow {
id: clideAbout
}
Action {
id: actionDocumentation
text: qsTr("&Documentation")
}
Action {
id: actionAbout
text: qsTr("&About")
// Toggle the about window with the menu item is clicked.
onTriggered: clideAbout.visible = !clideAbout.visible
}
ClideMenu {
title: qsTr("&Help")
ClideMenuItem {
action: actionDocumentation
}
ClideMenuItem {
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
}
}
}

112
qml/ClideProjectView.qml Normal file
View File

@@ -0,0 +1,112 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import clide.module 1.0
import Logger 1.0
SplitView {
id: root
// Path to the directory of the project opened in clide.
required property string projectDir
Layout.fillHeight: true
Layout.fillWidth: true
anchors.fill: parent
// 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
radius: 2.5
// Execute these behaviors when the color is changed.
Behavior on color {
ColorAnimation {
duration: 400
}
}
}
Rectangle {
id: navigationView
SplitView.fillHeight: true
SplitView.maximumWidth: 250
SplitView.minimumWidth: 0
SplitView.preferredWidth: 200
color: RustColors.explorer_background
ColumnLayout {
spacing: 2
// TODO: Make a ClideBreadCrumb element to support select parent paths as root
Rectangle {
color: RustColors.explorer_background
height: 25
width: navigationView.width
Text {
id: breadCrumb
anchors.fill: parent
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;
}
}
}
}
ClideTreeView {
id: clideTreeView
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;
}
}
}
}
ClideEditor {
id: clideEditor
SplitView.fillWidth: true
// Provide a path to the file currently open in the text editor.
// Initialized using the Default trait in Rust QML singleton FileSystem.
filePath: FileSystem.filePath
}
}

View File

@@ -10,30 +10,38 @@ import clide.module 1.0
import Logger 1.0 import Logger 1.0
TreeView { TreeView {
id: root id: fileSystemTreeView
property int lastIndex: -1 property int lastIndex: -1
required property string originalRootDirectory required property string originalRootDirectory
property string rootDirectory property string rootDirectory
property int rootIndent: 25
signal fileClicked(string filePath) signal fileClicked(string filePath)
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
boundsMovement: Flickable.StopAtBounds boundsMovement: Flickable.StopAtBounds
clip: true clip: true
leftMargin: 5
// The model is implemented in filesystem.rs
model: FileSystem model: FileSystem
// Set the root directory on the Rust model, returning the QModeIndex to use for the root of the tree view widget. rootIndex: FileSystem.setDirectory(fileSystemTreeView.rootDirectory)
rootIndex: FileSystem.setDirectory(root.rootDirectory)
// Provide our own custom ScrollIndicator for the TreeView. // Provide our own custom ScrollIndicator for the TreeView.
ScrollBar.horizontal: ClideScrollBar { ScrollIndicator.vertical: ScrollIndicator {
sizeModifier: 3 active: true
implicitWidth: 15
contentItem: Rectangle {
color: RustColors.scrollbar
implicitHeight: 6
implicitWidth: 6
opacity: fileSystemTreeView.movingVertically ? 0.5 : 0.0
Behavior on opacity {
OpacityAnimator {
duration: 500
}
}
} }
ScrollBar.vertical: ClideScrollBar {
sizeModifier: 3
} }
// The delegate represents a single entry in the filesystem. // The delegate represents a single entry in the filesystem.
@@ -45,65 +53,68 @@ TreeView {
required property int index required property int index
implicitHeight: 25 implicitHeight: 25
implicitWidth: root.width implicitWidth: fileSystemTreeView.width > 0 ? fileSystemTreeView.width : 250
indentation: 12 indentation: 12
background: Rectangle { background: Rectangle {
color: current ? RustColors.explorer_folder_open : "transparent" // TODO: Fix flickering from color transition on states here.
radius: 20 color: (treeDelegate.index === fileSystemTreeView.lastIndex) ? RustColors.explorer_text_selected : (hoverHandler.hovered ? RustColors.explorer_hovered : "transparent")
width: root.width opacity: hoverHandler.hovered ? 0.75 : 1.0
radius: 2.5
Behavior on color {
ColorAnimation {
duration: 300
}
}
} }
// Item name.
contentItem: Text { contentItem: Text {
anchors.left: itemIcon.right
anchors.leftMargin: 5
color: RustColors.explorer_text color: RustColors.explorer_text
text: treeDelegate.fileName text: treeDelegate.fileName
} }
// Item Icon. indicator: Image {
indicator: Label { id: directoryIcon
id: itemIcon
function setSourceImage() {
let folderOpen = "data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 576 512\"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d=\"M88.7 223.8L0 375.8 0 96C0 60.7 28.7 32 64 32l117.5 0c17 0 33.3 6.7 45.3 18.7l26.5 26.5c12 12 28.3 18.7 45.3 18.7L416 96c35.3 0 64 28.7 64 64l0 32-336 0c-22.8 0-43.8 12.1-55.3 31.8zm27.6 16.1C122.1 230 132.6 224 144 224l400 0c11.5 0 22 6.1 27.7 16.1s5.7 22.2-.1 32.1l-112 192C453.9 474 443.4 480 432 480L32 480c-11.5 0-22-6.1-27.7-16.1s-5.7-22.2 .1-32.1l112-192z\"/></svg>";
let folderClosed = "data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d=\"M64 480H448c35.3 0 64-28.7 64-64V160c0-35.3-28.7-64-64-64H288c-10.1 0-19.6-4.7-25.6-12.8L243.2 57.6C231.1 41.5 212.1 32 192 32H64C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64z\"/></svg>";
let file = "data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 384 512\"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d=\"M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 288c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128z\"/></svg>";
// If the item has children, it's a directory.
if (treeDelegate.hasChildren) {
return treeDelegate.expanded ? folderOpen : folderClosed;
}
return file;
}
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
antialiasing: true antialiasing: true
enabled: false asynchronous: true
focus: false fillMode: Image.PreserveAspectFit
font.family: localFont.font.family
font.pixelSize: 18
smooth: true smooth: true
// Get the icon from Rust implementation. source: setSourceImage()
text: root.model.icon(filePath) sourceSize.height: 15
x: root.rootIndent + (treeDelegate.depth * treeDelegate.indentation) + (carrotIndicator.visible ? carrotIndicator.width : 0) sourceSize.width: 15
x: treeDelegate.leftMargin + (treeDelegate.depth * treeDelegate.indentation)
} }
// Directory carrot indicator.
Label {
id: carrotIndicator
anchors.verticalCenter: parent.verticalCenter
font.family: localFont.font.family
font.pixelSize: 10
font.weight: localFont.font.weight
text: expanded ? "⮟" : "⮞"
visible: isTreeNode && hasChildren
x: (root.rootIndent - implicitWidth) + (depth * indentation)
}
// Apply colorization effects to the icon for the item.
MultiEffect { MultiEffect {
anchors.fill: itemIcon id: iconOverlay
anchors.fill: directoryIcon
brightness: 1.0 brightness: 1.0
colorization: 1.0 colorization: 1.0
colorizationColor: { colorizationColor: {
const isFile = !treeDelegate.hasChildren; const isFile = !treeDelegate.hasChildren;
if (isFile) if (isFile)
return Qt.lighter(RustColors.explorer_folder, 2); return Qt.lighter(RustColors.explorer_folder, 2);
const isExpandedFolder = treeDelegate.expanded && treeDelegate.hasChildren; const isExpandedFolder = treeDelegate.expanded && treeDelegate.hasChildren;
if (isExpandedFolder) if (isExpandedFolder)
return Qt.darker(RustColors.explorer_folder, 2); return Qt.darker(RustColors.explorer_folder, 2);
else else
return RustColors.explorer_folder; return RustColors.explorer_folder;
} }
source: itemIcon source: directoryIcon
} }
HoverHandler { HoverHandler {
id: hoverHandler id: hoverHandler
@@ -116,12 +127,11 @@ TreeView {
onSingleTapped: (eventPoint, button) => { onSingleTapped: (eventPoint, button) => {
switch (button) { switch (button) {
case Qt.LeftButton: case Qt.LeftButton:
if (treeDelegate.hasChildren) { fileSystemTreeView.toggleExpanded(treeDelegate.row);
root.toggleExpanded(treeDelegate.row); // If this model item doesn't have children, it means it's
} else { // representing a file.
// If this model item doesn't have children, it means it's representing a file. if (!treeDelegate.hasChildren)
root.fileClicked(treeDelegate.filePath); fileSystemTreeView.fileClicked(treeDelegate.filePath);
}
break; break;
case Qt.RightButton: case Qt.RightButton:
contextMenu.popup(); contextMenu.popup();
@@ -129,38 +139,26 @@ TreeView {
} }
} }
} }
ClideMenu { Menu {
id: contextMenu id: contextMenu
ClideMenuItem { Action {
action: Action {
enabled: treeDelegate.hasChildren enabled: treeDelegate.hasChildren
text: qsTr("Set root") text: qsTr("Set as root index")
onTriggered: { onTriggered: {
Logger.debug("Setting new root directory: " + treeDelegate.filePath); Logger.debug("Setting new root directory: " + treeDelegate.filePath);
root.rootDirectory = treeDelegate.filePath; fileSystemTreeView.rootDirectory = treeDelegate.filePath;
} }
} }
} Action {
ClideMenuItem { text: qsTr("Reset root index")
action: Action {
text: qsTr("Reset root")
onTriggered: { onTriggered: {
Logger.log("Resetting root directory: " + root.originalRootDirectory); Logger.log("Resetting root directory: " + fileSystemTreeView.originalRootDirectory);
root.rootDirectory = root.originalRootDirectory; fileSystemTreeView.rootDirectory = fileSystemTreeView.originalRootDirectory;
} }
} }
} }
} }
}
selectionModel: ItemSelectionModel {
}
FontLoader {
id: localFont
source: "qrc:/fonts/saucecodepro-xlight.ttf"
}
} }

View File

@@ -1,107 +0,0 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import clide.module 1.0
import Logger 1.0
Rectangle {
id: root
property var fullPaths: []
required property string path
property var segments: []
signal crumbClicked(string path)
signal resetRoot
function rebuildSegments(): string {
let cleaned = path;
if (cleaned.endsWith("/"))
cleaned = cleaned.slice(0, -1);
Logger.trace("Building segments for path: " + cleaned);
segments = ["/"];
fullPaths = ["/"];
let parts = cleaned.split("/");
let current = "";
// We already pushed the root `/` path during initialization, so skip index 0.
for (let i = 1; i < parts.length; ++i) {
current += "/" + parts[i];
Logger.trace("Pushing path: " + parts[i] + " Current: " + current);
segments.push(parts[i]);
fullPaths.push(current);
}
// Update the model used in the Repeater to show the new segments.
repeater.model = segments;
}
color: "transparent"
implicitHeight: breadcrumbRow.implicitHeight
width: parent.width
Component.onCompleted: rebuildSegments()
onPathChanged: rebuildSegments()
Flow {
id: breadcrumbRow
anchors.fill: parent
spacing: 2
width: parent.width
Repeater {
id: repeater
model: root.segments
delegate: Text {
required property int index
required property string modelData
function getText(): string {
if (modelData === "/") {
return modelData;
}
return modelData + "/";
}
// Show blue underlined hyperlink text if the mouse is hovering a segment.
color: mouseArea.containsMouse ? "#2a7fff" : RustColors.explorer_text
font.underline: mouseArea.containsMouse
text: getText()
// Click events for each path segment call signal so the parent can set the file explorer root path.
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
Logger.info(index + "] Breadcrumb clicked:" + root.fullPaths[index]);
crumbClicked(root.fullPaths[index]);
}
}
}
}
}
TapHandler {
acceptedButtons: Qt.RightButton
onSingleTapped: contextMenu.popup()
}
ClideMenu {
id: contextMenu
ClideMenuItem {
action: Action {
text: qsTr("Reset root")
onTriggered: {
Logger.info("Resetting root directory from ClideBreadCrumbs");
resetRoot();
}
}
}
}
}

View File

@@ -8,15 +8,13 @@ import QtQuick.Controls
import clide.module 1.0 import clide.module 1.0
Rectangle { Rectangle {
id: root
readonly property color currentColor: { readonly property color currentColor: {
if (pressed) { if (pressed) {
return RustColors.pressed; return RustColors.pressed;
} else if (hovered) { } else if (hovered) {
return RustColors.hovered; return RustColors.hovered;
} else { } else {
return "transparent"; return RustColors.gutter;
} }
} }
required property bool hovered required property bool hovered
@@ -25,20 +23,12 @@ Rectangle {
border.color: currentColor border.color: currentColor
color: currentColor color: currentColor
implicitHeight: 8 implicitHeight: 8
implicitWidth: 8
radius: 2.5 radius: 2.5
opacity: root.hovered ? 1.0 : 0.0
// Execute these behaviors when the color is changed. // Execute these behaviors when the color is changed.
Behavior on color { Behavior on color {
ColorAnimation { ColorAnimation {
duration: 500 duration: 400
}
}
Behavior on opacity {
OpacityAnimator {
duration: 500
} }
} }
} }

View File

@@ -1,14 +0,0 @@
import QtQuick
import QtQuick.Controls.Basic
import clide.module 1.0
Menu {
background: Rectangle {
border.color: RustColors.hovered
border.width: 10
color: RustColors.menubar
implicitWidth: 100
radius: 5
}
}

View File

@@ -1,153 +0,0 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
import QtQuick
import QtQuick.Controls
import clide.module 1.0
MenuBar {
// Background for this MenuBar.
background: Rectangle {
color: RustColors.menubar
}
//
// File Menu
ClideMenu {
title: qsTr("&File")
ClideMenuItem {
action: Action {
id: actionNewProject
text: qsTr("&New Project...")
}
}
ClideMenuItem {
action: Action {
id: actionOpen
text: qsTr("&Open...")
}
onTriggered: FileSystem.setDirectory(FileSystem.filePath)
}
ClideMenuItem {
action: Action {
id: actionSave
text: qsTr("&Save")
}
}
MenuSeparator {
background: Rectangle {
border.color: color
color: Qt.darker(RustColors.menubar, 1)
implicitHeight: 3
implicitWidth: 200
}
}
ClideMenuItem {
action: Action {
id: actionExit
text: qsTr("&Exit")
onTriggered: Qt.quit()
}
}
}
//
// Edit Menu
ClideMenu {
title: qsTr("&Edit")
ClideMenuItem {
action: Action {
id: actionUndo
text: qsTr("&Undo")
}
}
ClideMenuItem {
action: Action {
id: actionRedo
text: qsTr("&Redo")
}
}
ClideMenuItem {
action: Action {
id: actionCut
text: qsTr("&Cut")
}
}
ClideMenuItem {
action: Action {
id: actionCopy
text: qsTr("&Copy")
}
}
ClideMenuItem {
action: Action {
id: actionPaste
text: qsTr("&Paste")
}
}
}
//
// View Menu
ClideMenu {
title: qsTr("&View")
ClideMenuItem {
action: Action {
id: actionAppearance
text: qsTr("&Appearance")
}
}
ClideMenuItem {
action: Action {
id: actionToolWindows
text: qsTr("&Tool Windows")
}
}
}
//
// Help Menu
ClideAboutWindow {
id: clideAbout
}
ClideMenu {
title: qsTr("&Help")
ClideMenuItem {
action: Action {
id: actionDocumentation
text: qsTr("&Documentation")
}
}
ClideMenuItem {
action: Action {
id: actionAbout
text: qsTr("&About")
// Toggle the about window with the menu item is clicked.
onTriggered: clideAbout.visible = !clideAbout.visible
}
}
}
}

View File

@@ -1,18 +0,0 @@
import QtQuick
import QtQuick.Controls.Basic
import clide.module 1.0
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
}
}

View File

@@ -11,10 +11,9 @@ ScrollBar {
id: scrollBar id: scrollBar
// Height, opacitiy, width // Height, opacitiy, width
property int h: scrollBar.interactive ? sizeModifier * 2 : sizeModifier property int h: scrollBar.interactive ? 4 * 2 : 4
property int o: scrollBar.active && scrollBar.size < 1.0 ? 1.0 : 0.0 property int o: scrollBar.active && scrollBar.size < 1.0 ? 1.0 : 0.0
property int sizeModifier: 4 property int w: scrollBar.interactive ? 4 * 2 : 4
property int w: scrollBar.interactive ? sizeModifier * 2 : sizeModifier
// Scroll bar gutter // Scroll bar gutter
background: Rectangle { background: Rectangle {
@@ -26,7 +25,6 @@ ScrollBar {
// Fade the scrollbar gutter when inactive. // Fade the scrollbar gutter when inactive.
opacity: scrollBar.o opacity: scrollBar.o
radius: 20
Behavior on opacity { Behavior on opacity {
OpacityAnimator { OpacityAnimator {
@@ -57,7 +55,6 @@ ScrollBar {
// Fade the scrollbar when inactive. // Fade the scrollbar when inactive.
opacity: scrollBar.o opacity: scrollBar.o
radius: 20
// Smooth transition between color changes based on the state above. // Smooth transition between color changes based on the state above.
Behavior on color { Behavior on color {

View File

@@ -1,9 +1,2 @@
ClideScrollBar ClideScrollBar.qml ClideScrollBar ClideScrollBar.qml
ClideHandle ClideHandle.qml ClideHandle ClideHandle.qml
ClideMenu ClideMenu.qml
ClideMenuItem ClideMenuItem.qml
ClideBreadCrumbs ClideBreadCrumbs.qml
ClideAboutWindow ClideAboutWindow.qml
ClideEditor ClideEditor.qml
ClideLogger ClideLogger.qml
ClideMenuBar ClideMenuBar.qml

View File

@@ -15,29 +15,22 @@ ApplicationWindow {
required property string appContextPath required property string appContextPath
height: 800 height: 800
title: "Clide" title: "CLIDE"
visible: true visible: true
width: 1200 width: 1200
menuBar: ClideMenuBar { menuBar: ClideMenuBar {
} }
Rectangle { MessageDialog {
color: RustColors.menubar id: errorDialog
width: appView.implicitWidth
height: appView.implicitHeight
ClideApplicationView { title: qsTr("Error")
id: appView
projectDir: appWindow.appContextPath
implicitHeight: appWindow.height
implicitWidth: appWindow.width
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.leftMargin: 20
anchors.topMargin: 10
} }
ClideProjectView {
id: clideProjectView
projectDir: appWindow.appContextPath
} }
} }

View File

@@ -1,10 +1,5 @@
<RCC> <RCC>
<qresource prefix="/images"> <qresource prefix="/images">
<file alias="kilroy.png">resources/images/kilroy-256.png</file> <file alias="kilroy.png">images/kilroy-256.png</file>
</qresource>
<qresource prefix="/fonts">
<file alias="saucecodepro.ttf">resources/SauceCodeProNerdFont-Black.ttf</file>
<file alias="saucecodepro-light.ttf">resources/SauceCodeProNerdFont-Light.ttf</file>
<file alias="saucecodepro-xlight.ttf">resources/SauceCodeProNerdFont-ExtraLight.ttf</file>
</qresource> </qresource>
</RCC> </RCC>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 402 KiB

View File

@@ -93,7 +93,7 @@ impl Default for RustColorsImpl {
linenumber: QColor::try_from("#94989b").unwrap(), linenumber: QColor::try_from("#94989b").unwrap(),
active: QColor::try_from("#a9acb0").unwrap(), active: QColor::try_from("#a9acb0").unwrap(),
inactive: QColor::try_from("#FFF").unwrap(), inactive: QColor::try_from("#FFF").unwrap(),
editor_background: QColor::try_from("#1E1F22").unwrap(), editor_background: QColor::try_from("#111111").unwrap(),
editor_text: QColor::try_from("#acaea3").unwrap(), editor_text: QColor::try_from("#acaea3").unwrap(),
editor_highlighted_text: QColor::try_from("#ccced3").unwrap(), editor_highlighted_text: QColor::try_from("#ccced3").unwrap(),
editor_highlight: QColor::try_from("#ccced3").unwrap(), editor_highlight: QColor::try_from("#ccced3").unwrap(),
@@ -103,7 +103,7 @@ impl Default for RustColorsImpl {
explorer_text_selected: QColor::try_from("#262626").unwrap(), explorer_text_selected: QColor::try_from("#262626").unwrap(),
explorer_background: QColor::try_from("#1E1F22").unwrap(), explorer_background: QColor::try_from("#1E1F22").unwrap(),
explorer_folder: QColor::try_from("#54585b").unwrap(), explorer_folder: QColor::try_from("#54585b").unwrap(),
explorer_folder_open: QColor::try_from("#393B40").unwrap(), explorer_folder_open: QColor::try_from("#2b2b2b").unwrap(),
terminal_background: QColor::try_from("#111111").unwrap(), terminal_background: QColor::try_from("#111111").unwrap(),
info_log: QColor::try_from("#C4FFFF").unwrap(), info_log: QColor::try_from("#C4FFFF").unwrap(),
debug_log: QColor::try_from("#9148AF").unwrap(), debug_log: QColor::try_from("#9148AF").unwrap(),

View File

@@ -3,14 +3,15 @@
// SPDX-License-Identifier: GNU General Public License v3.0 or later // SPDX-License-Identifier: GNU General Public License v3.0 or later
use cxx_qt_lib::{QModelIndex, QString}; use cxx_qt_lib::{QModelIndex, QString};
use devicons::FileIcon;
use dirs; use dirs;
use log::warn; use log::warn;
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
use syntect::easy::HighlightLines; use syntect::easy::HighlightLines;
use syntect::highlighting::ThemeSet; use syntect::highlighting::ThemeSet;
use syntect::html::{IncludeBackground, append_highlighted_html_for_styled_line}; use syntect::html::{
IncludeBackground, append_highlighted_html_for_styled_line, start_highlighted_html_snippet,
};
use syntect::parsing::SyntaxSet; use syntect::parsing::SyntaxSet;
use syntect::util::LinesWithEndings; use syntect::util::LinesWithEndings;
@@ -51,12 +52,10 @@ pub mod qobject {
#[qinvokable] #[qinvokable]
#[cxx_name = "setDirectory"] #[cxx_name = "setDirectory"]
fn set_directory(self: Pin<&mut FileSystem>, path: &QString) -> QModelIndex; fn set_directory(self: Pin<&mut FileSystem>, path: &QString) -> QModelIndex;
#[qinvokable]
fn icon(self: Pin<&mut FileSystem>, path: &QString) -> QString;
} }
} }
// TODO: Implement a provider for QFileSystemModel::setIconProvider for icons.
pub struct FileSystemImpl { pub struct FileSystemImpl {
file_path: QString, file_path: QString,
} }
@@ -96,8 +95,7 @@ impl qobject::FileSystem {
) )
.unwrap_or_else(|| ss.find_syntax_plain_text()); .unwrap_or_else(|| ss.find_syntax_plain_text());
let mut highlighter = HighlightLines::new(lang, theme); let mut highlighter = HighlightLines::new(lang, theme);
// If you care about the background, see `start_highlighted_html_snippet(theme);`. let (mut output, _bg) = start_highlighted_html_snippet(theme);
let mut output = String::from("<pre>\n");
for line in LinesWithEndings::from(lines.as_str()) { for line in LinesWithEndings::from(lines.as_str()) {
let regions = highlighter let regions = highlighter
.highlight_line(line, &ss) .highlight_line(line, &ss)
@@ -105,7 +103,7 @@ impl qobject::FileSystem {
append_highlighted_html_for_styled_line( append_highlighted_html_for_styled_line(
&regions[..], &regions[..],
IncludeBackground::No, IncludeBackground::Yes,
&mut output, &mut output,
) )
.expect("Failed to insert highlighted html"); .expect("Failed to insert highlighted html");
@@ -141,15 +139,4 @@ impl qobject::FileSystem {
self.set_root_path(&QString::from(homedir)) self.set_root_path(&QString::from(homedir))
} }
} }
fn icon(self: std::pin::Pin<&mut Self>, path: &QString) -> QString {
let str = path.to_string();
if Path::new(&str).is_dir() {
// Ensures directories are given a folder icon and not mistakenly resolved to a language.
// For example, a directory named `cpp` would otherwise return a C++ icon.
return QString::from(FileIcon::from("dir/").to_string())
}
let icon = FileIcon::from(str);
QString::from(icon.to_string())
}
} }