31 Commits

Author SHA1 Message Date
e3bf484a5d Remove TODO 2026-02-08 16:21:52 -05:00
bedd510b0a Update GUI screenshot. 2026-02-08 15:46:17 -05:00
a30ea2b457 Remove TODOs. 2026-02-08 15:37:24 -05:00
52ceda5b06 Reorganize files. 2026-02-08 15:29:34 -05:00
a37fd74d32 Fix spacing. 2026-02-08 15:25:21 -05:00
6ed4ca11f4 Fix width for file tree selection. 2026-02-08 15:21:06 -05:00
41fab2847d Update GUI screenshot in README. 2026-02-08 15:10:06 -05:00
c76529b3cb Fix first line background.
Added a comment if the background in desired in the future.
2026-02-08 15:02:28 -05:00
5f5b1a4d39 Add frame around the application view. 2026-02-08 14:52:57 -05:00
4bb4ce1882 Clean up GUI. 2026-02-08 14:26:26 -05:00
344efc0042 Clean up ClideHandle. 2026-02-08 10:08:47 -05:00
a63acb18fc WIP 2026-02-08 00:03:50 -05:00
f918d65888 Add clickable bread crumbs.
Clicking a parent path changes the root project directory.
2026-02-07 21:18:59 -05:00
aa42ec6072 Get file explorer icon based on file type.
Fixes #15
2026-02-07 18:41:51 -05:00
6f2a655497 Clean up context menus. 2026-02-07 12:59:44 -05:00
c0f38b531d Add arrow to show expanded folder. 2026-02-07 12:43:15 -05:00
4aad91416e Fix selection flickering when expanding folder. 2026-02-07 11:16:57 -05:00
ff1b5ab2e6 Hide handles when not in use. 2026-02-07 10:51:37 -05:00
491087a6c1 Add environment variables to config.toml 2026-02-07 10:37:05 -05:00
c170b3b20d WIP 2026-02-06 22:10:20 -05:00
125041f469 Add ClideEditorView. 2026-02-06 19:36:14 -05:00
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
34 changed files with 1054 additions and 671 deletions

View File

@@ -1,3 +1,6 @@
[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"

33
.qmllint.ini Normal file
View File

@@ -0,0 +1,33 @@
[General]
DisableDefaultImports=false
[Warnings]
AccessSingletonViaObject=warning
AttachedPropertyReuse=disable
BadSignalHandlerParameters=warning
CompilerWarnings=disable
Deprecated=warning
DuplicatePropertyBinding=warning
DuplicatedName=warning
ImportFailure=warning
IncompatibleType=warning
InheritanceCycle=warning
InvalidLintDirective=warning
LintPluginWarnings=disable
MissingProperty=warning
MissingType=warning
MultilineStrings=info
NonListProperty=warning
PrefixedImportType=warning
PropertyAliasCycles=warning
ReadOnlyProperty=warning
RequiredProperty=warning
RestrictedType=warning
TopLevelComponent=warning
UncreatableType=warning
UnqualifiedAccess=warning
UnresolvedType=warning
UnusedImports=info
UseProperFunction=warning
VarUsedBeforeDeclaration=warning
WithStatement=warning

10
Cargo.lock generated
View File

@@ -295,6 +295,7 @@ dependencies = [
"cxx-qt", "cxx-qt",
"cxx-qt-build", "cxx-qt-build",
"cxx-qt-lib", "cxx-qt-lib",
"devicons",
"dirs", "dirs",
"edtui", "edtui",
"log", "log",
@@ -667,6 +668,15 @@ 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,6 +18,7 @@ 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,13 +19,14 @@ 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. To check your Qt version This project requires at least Qt 6.7.3 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
@@ -35,6 +36,14 @@ 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
@@ -98,6 +107,8 @@ 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,12 +2,21 @@ 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/main.qml", "qml/ClideApplicationView.qml",
"qml/ClideAboutWindow.qml", "qml/ClideEditorView.qml",
"qml/ClideExplorerView.qml",
"qml/ClideTreeView.qml", "qml/ClideTreeView.qml",
"qml/ClideProjectView.qml", "qml/Components/ClideAboutWindow.qml",
"qml/ClideEditor.qml", "qml/Components/ClideBreadCrumbs.qml",
"qml/ClideMenuBar.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",
])) ]))
// Link Qt's Network library // Link Qt's Network library
// - Qt Core is always linked // - Qt Core is always linked

View File

@@ -0,0 +1,46 @@
// 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

@@ -1,211 +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
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
// Customized handle to drag between the Editor and the Console.
handle: Rectangle {
border.color: SplitHandle.pressed ? RustColors.pressed : SplitHandle.hovered ? RustColors.hovered : RustColors.gutter
color: SplitHandle.pressed ? RustColors.pressed : SplitHandle.hovered ? RustColors.hovered : RustColors.gutter
implicitHeight: 8
radius: 2.5
// Execute these behaviors when the color is changed.
Behavior on color {
ColorAnimation {
duration: 400
}
}
}
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.
// We need to ensure space required to show N line number digits.
// We use log10 to find how many digits are needed in a line number.
// log10(9) ~= .95; log10(10) = 1.0; log10(100) = 2.0 ...etc
// We +1 to ensure space for at least 1 digit, as floor(1.95) = 1.
// The +10 is additional spacing and can be adjusted.
Layout.preferredWidth: fontMetrics.averageCharacterWidth * (Math.floor(Math.log10(textArea.lineCount)) + 1) + 10
contentY: editorFlickable.contentY
interactive: false
Column {
anchors.fill: parent
topPadding: textArea.topPadding
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 {
// 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
horizontalAlignment: Text.AlignLeft
text: parent.index + 1
verticalAlignment: Text.AlignVCenter
width: parent.width - indicator.width
}
// Draw edge along the right side of the line number.
Rectangle {
id: indicator
anchors.left: numbers.right
color: RustColors.linenumber
height: parent.height
width: 1
}
}
}
}
}
Flickable {
id: editorFlickable
Layout.fillHeight: true
Layout.fillWidth: true
boundsBehavior: Flickable.StopAtBounds
height: 650
ScrollBar.horizontal: MyScrollBar {
}
ScrollBar.vertical: MyScrollBar {
}
TextArea.flickable: TextArea {
id: textArea
focus: true
persistentSelection: true
antialiasing: true
selectByMouse: true
selectionColor: RustColors.editor_highlight
selectedTextColor: RustColors.editor_highlighted_text
textFormat: Qt.AutoText
wrapMode: TextArea.Wrap
text: FileSystem.readFile(root.filePath)
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: textArea.font
}
}
}
TextArea {
id: areaConsole
height: 100
width: parent.width
placeholderText: qsTr("shaun@pc:~/Code/clide$ ")
placeholderTextColor: "white"
readOnly: true
wrapMode: TextArea.Wrap
background: Rectangle {
color: RustColors.terminal_background
implicitHeight: 100
// border.color: control.enabled ? RustColors.active : RustColors.inactive
}
}
// 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
// Scroll bar gutter
background: Rectangle {
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
}
}
}
// 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
// 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
}
}
}
}
}

49
qml/ClideEditorView.qml Normal file
View File

@@ -0,0 +1,49 @@
// 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 {
}
}
}

62
qml/ClideExplorerView.qml Normal file
View File

@@ -0,0 +1,62 @@
// 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

@@ -1,189 +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
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
}
}
//
// 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
// Toggle the about window with the menu item is clicked.
onTriggered: clideAbout.visible = !clideAbout.visible
text: qsTr("&About")
}
ClideMenu {
title: qsTr("&Help")
ClideMenuItem {
action: actionDocumentation
}
ClideMenuItem {
action: actionAbout
}
}
}

View File

@@ -1,106 +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
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
color: RustColors.explorer_background
SplitView.fillHeight: true
SplitView.minimumWidth: 0
SplitView.preferredWidth: 200
SplitView.maximumWidth: 250
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
Text {
id: breadCrumb
anchors.fill: parent
text: clideTreeView.rootDirectory
color: RustColors.explorer_text
elide: Text.ElideLeft
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
TapHandler {
acceptedButtons: Qt.RightButton
onSingleTapped: (eventPoint, button) => {
contextMenu.popup()
}
}
Menu {
id: contextMenu
Action {
text: qsTr("Reset root index")
onTriggered: {
console.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
onRootDirectoryChanged: {
console.log(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

@@ -7,118 +7,121 @@ import QtQuick.Effects
import QtQuick.Controls import QtQuick.Controls
import clide.module 1.0 import clide.module 1.0
import Logger 1.0
TreeView { TreeView {
id: fileSystemTreeView id: root
model: FileSystem
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)
rootIndex: FileSystem.setDirectory(fileSystemTreeView.rootDirectory)
leftMargin: 5
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
boundsMovement: Flickable.StopAtBounds boundsMovement: Flickable.StopAtBounds
clip: true clip: true
// The model is implemented in filesystem.rs
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(root.rootDirectory)
// Provide our own custom ScrollIndicator for the TreeView.
ScrollBar.horizontal: ClideScrollBar {
sizeModifier: 3
}
ScrollBar.vertical: ClideScrollBar {
sizeModifier: 3
}
// The delegate represents a single entry in the filesystem. // The delegate represents a single entry in the filesystem.
delegate: TreeViewDelegate { delegate: TreeViewDelegate {
id: treeDelegate 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 required property string fileName
required property url filePath
required property int index
indicator: Image { implicitHeight: 25
id: directoryIcon implicitWidth: root.width
indentation: 12
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;
} else {
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 { background: Rectangle {
// TODO: Fix flickering from color transition on states here. color: current ? RustColors.explorer_folder_open : "transparent"
color: (treeDelegate.index === fileSystemTreeView.lastIndex) radius: 20
? RustColors.explorer_text_selected width: root.width
: (hoverHandler.hovered ? RustColors.explorer_hovered : "transparent") }
radius: 2.5 // Item name.
opacity: hoverHandler.hovered ? 0.75 : 1.0 contentItem: Text {
anchors.left: itemIcon.right
anchors.leftMargin: 5
color: RustColors.explorer_text
text: treeDelegate.fileName
}
// Item Icon.
indicator: Label {
id: itemIcon
Behavior on color { anchors.verticalCenter: parent.verticalCenter
ColorAnimation { antialiasing: true
duration: 300 enabled: false
} focus: false
} font.family: localFont.font.family
font.pixelSize: 18
smooth: true
// Get the icon from Rust implementation.
text: root.model.icon(filePath)
x: root.rootIndent + (treeDelegate.depth * treeDelegate.indentation) + (carrotIndicator.visible ? carrotIndicator.width : 0)
} }
// 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 {
id: iconOverlay anchors.fill: itemIcon
anchors.fill: directoryIcon
source: directoryIcon
colorization: 1.0
brightness: 1.0 brightness: 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
} }
HoverHandler { HoverHandler {
id: hoverHandler id: hoverHandler
acceptedDevices: PointerDevice.Mouse acceptedDevices: PointerDevice.Mouse
} }
TapHandler { TapHandler {
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
onSingleTapped: (eventPoint, button) => { onSingleTapped: (eventPoint, button) => {
switch (button) { switch (button) {
case Qt.LeftButton: case Qt.LeftButton:
fileSystemTreeView.toggleExpanded(treeDelegate.row) if (treeDelegate.hasChildren) {
// If this model item doesn't have children, it means it's root.toggleExpanded(treeDelegate.row);
// representing a file. } else {
if (!treeDelegate.hasChildren) // If this model item doesn't have children, it means it's representing a file.
fileSystemTreeView.fileClicked(treeDelegate.filePath) root.fileClicked(treeDelegate.filePath);
}
break; break;
case Qt.RightButton: case Qt.RightButton:
contextMenu.popup(); contextMenu.popup();
@@ -126,44 +129,38 @@ TreeView {
} }
} }
} }
ClideMenu {
Menu {
id: contextMenu id: contextMenu
Action {
text: qsTr("Set as root index") ClideMenuItem {
action: Action {
enabled: treeDelegate.hasChildren enabled: treeDelegate.hasChildren
text: qsTr("Set root")
onTriggered: { onTriggered: {
console.log("Setting new root directory: " + treeDelegate.filePath) Logger.debug("Setting new root directory: " + treeDelegate.filePath);
fileSystemTreeView.rootDirectory = treeDelegate.filePath root.rootDirectory = treeDelegate.filePath;
} }
} }
Action { }
text: qsTr("Reset root index") ClideMenuItem {
action: Action {
text: qsTr("Reset root")
onTriggered: { onTriggered: {
console.log("Resetting root directory: " + fileSystemTreeView.originalRootDirectory) Logger.log("Resetting root directory: " + root.originalRootDirectory);
fileSystemTreeView.rootDirectory = fileSystemTreeView.originalRootDirectory root.rootDirectory = root.originalRootDirectory;
} }
} }
} }
} }
}
selectionModel: ItemSelectionModel {
}
// Provide our own custom ScrollIndicator for the TreeView. FontLoader {
ScrollIndicator.vertical: ScrollIndicator { id: localFont
active: true
implicitWidth: 15
contentItem: Rectangle { source: "qrc:/fonts/saucecodepro-xlight.ttf"
implicitWidth: 6
implicitHeight: 6
color: RustColors.scrollbar
opacity: fileSystemTreeView.movingVertically ? 0.5 : 0.0
Behavior on opacity {
OpacityAnimator {
duration: 500
}
}
}
} }
} }

View File

@@ -6,18 +6,22 @@ import QtQuick
import QtQuick.Controls.Basic import QtQuick.Controls.Basic
import clide.module 1.0 import clide.module 1.0
import Logger 1.0
ApplicationWindow { ApplicationWindow {
id: root id: root
width: 450
height: 350
// Create the window with no frame and keep it on top.
flags: Qt.Window | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
color: RustColors.gutter color: RustColors.gutter
// Create the window with no frame and keep it on top.
flags: Qt.Tool | Qt.FramelessWindowHint
height: 350
width: 450
visible: root.active
// Hide the window when it loses focus. // Hide the window when it loses focus.
onActiveChanged: { onActiveChanged: {
if (!active) { Logger.debug("Setting active: " + root.active)
if (!root.active) {
root.visible = false; root.visible = false;
} }
} }
@@ -27,48 +31,38 @@ ApplicationWindow {
id: logo id: logo
anchors.left: parent.left anchors.left: parent.left
anchors.margins: 20
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top 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 antialiasing: true
asynchronous: true asynchronous: true
fillMode: Image.PreserveAspectFit
smooth: true
source: "qrc:/images/kilroy.png"
sourceSize.height: 80
sourceSize.width: 80
} }
ScrollView { ScrollView {
anchors.top: logo.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.margins: 20 anchors.margins: 20
anchors.right: parent.right
anchors.top: logo.bottom
TextArea { TextArea {
selectedTextColor: RustColors.editor_highlighted_text
selectionColor: RustColors.editor_highlight
horizontalAlignment: Text.AlignHCenter
textFormat: Text.RichText
text: qsTr("<h3>About CLIDE</h3>"
+ "<p>A simple text editor written in Rust and QML using CXX-Qt.</p>"
+ "<p>Personal website <a href=\"http://shaunreed.com\">shaunreed.com</a></p>"
+ "<p>Project notes <a href=\"http://knoats.com\">knoats.com</a></p>"
+ "<p>This project is developed at <a href=\"http://git.shaunreed.com/shaunrd0/clide\">git.shaunreed.com</a></p>"
+ "<p><a href=\"https://github.com/KDAB/cxx-qt\">KDAB CXX-Qt repository</a></p>"
+ "<p>Copyright (C) 2026 Shaun Reed, all rights reserved.</p>")
color: RustColors.editor_text
wrapMode: Text.WordWrap
readOnly: true
antialiasing: true antialiasing: true
background: null background: null
color: RustColors.editor_text
horizontalAlignment: Text.AlignHCenter
readOnly: true
selectedTextColor: RustColors.editor_highlighted_text
selectionColor: RustColors.editor_highlight
text: qsTr("<h3>About CLIDE</h3>" + "<p>A simple text editor written in Rust and QML using CXX-Qt.</p>" + "<p>Personal website <a href=\"http://shaunreed.com\">shaunreed.com</a></p>" + "<p>Project notes <a href=\"http://knoats.com\">knoats.com</a></p>" + "<p>This project is developed at <a href=\"http://git.shaunreed.com/shaunrd0/clide\">git.shaunreed.com</a></p>" + "<p><a href=\"https://github.com/KDAB/cxx-qt\">KDAB CXX-Qt repository</a></p>" + "<p>Copyright (C) 2026 Shaun Reed, all rights reserved.</p>")
textFormat: Text.RichText
wrapMode: Text.WordWrap
onLinkActivated: function (link) { onLinkActivated: function (link) {
Qt.openUrlExternally(link) Qt.openUrlExternally(link);
} }
} }
} }

View File

@@ -0,0 +1,107 @@
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

@@ -0,0 +1,116 @@
// 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 {
color: RustColors.editor_background
RowLayout {
anchors.fill: parent
// 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.
// We need to ensure space required to show N line number digits.
// We use log10 to find how many digits are needed in a line number.
// log10(9) ~= .95; log10(10) = 1.0; log10(100) = 2.0 ...etc
// We +1 to ensure space for at least 1 digit, as floor(1.95) = 1.
// The +10 is additional spacing and can be adjusted.
Layout.preferredWidth: fontMetrics.averageCharacterWidth * (Math.floor(Math.log10(textArea.lineCount)) + 1) + 10
contentY: editorFlickable.contentY
interactive: false
Column {
anchors.fill: parent
topPadding: textArea.topPadding
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
// Show the line number.
Label {
id: numbers
color: RustColors.linenumber
font: textArea.font
height: parent.height
horizontalAlignment: Text.AlignLeft
text: parent.index + 1
verticalAlignment: Text.AlignVCenter
width: parent.width - indicator.width
}
// Draw an edge along the right side of the line number.
Rectangle {
id: indicator
anchors.left: numbers.right
color: RustColors.linenumber
height: parent.height
width: 1
}
}
}
}
}
Flickable {
id: editorFlickable
Layout.fillHeight: true
Layout.fillWidth: true
boundsBehavior: Flickable.StopAtBounds
height: 650
ScrollBar.horizontal: ClideScrollBar {
}
ScrollBar.vertical: ClideScrollBar {
}
TextArea.flickable: TextArea {
id: textArea
antialiasing: true
focus: true
persistentSelection: true
selectByMouse: true
selectedTextColor: RustColors.editor_highlighted_text
selectionColor: RustColors.editor_highlight
text: FileSystem.readFile(root.filePath)
textFormat: Qt.AutoText
wrapMode: TextArea.Wrap
onLinkActivated: function (link) {
Qt.openUrlExternally(link);
}
}
FontMetrics {
id: fontMetrics
font: textArea.font
}
}
}
}

View File

@@ -0,0 +1,44 @@
// 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
Rectangle {
id: root
readonly property color currentColor: {
if (pressed) {
return RustColors.pressed;
} else if (hovered) {
return RustColors.hovered;
} else {
return "transparent";
}
}
required property bool hovered
required property bool pressed
border.color: currentColor
color: currentColor
implicitHeight: 8
implicitWidth: 8
radius: 2.5
opacity: root.hovered ? 1.0 : 0.0
// Execute these behaviors when the color is changed.
Behavior on color {
ColorAnimation {
duration: 500
}
}
Behavior on opacity {
OpacityAnimator {
duration: 500
}
}
}

View File

@@ -0,0 +1,60 @@
// 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
import Logger 1.0
Rectangle {
color: RustColors.terminal_background
radius: 10
ListModel {
id: model
}
ListView {
id: listView
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;
}
}
anchors.fill: parent
model: model
delegate: Text {
color: listView.getLogColor(level)
font.family: "monospace"
text: `[${level}] ${message}`
}
onCountChanged: Qt.callLater(positionViewAtEnd)
}
Connections {
function onLogged(level, message) {
model.append({
level,
message
});
}
target: Logger
}
}

View File

@@ -0,0 +1,14 @@
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

@@ -0,0 +1,153 @@
// 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

@@ -0,0 +1,18 @@
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

@@ -0,0 +1,74 @@
// 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
ScrollBar {
id: scrollBar
// Height, opacitiy, width
property int h: scrollBar.interactive ? sizeModifier * 2 : sizeModifier
property int o: scrollBar.active && scrollBar.size < 1.0 ? 1.0 : 0.0
property int sizeModifier: 4
property int w: scrollBar.interactive ? sizeModifier * 2 : sizeModifier
// Scroll bar gutter
background: Rectangle {
id: gutter
color: RustColors.scrollbar_gutter
implicitHeight: scrollBar.h
implicitWidth: scrollBar.w
// Fade the scrollbar gutter when inactive.
opacity: scrollBar.o
radius: 20
Behavior on opacity {
OpacityAnimator {
duration: 500
}
}
}
// Scroll bar
contentItem: Rectangle {
readonly property color barColor: {
if (scrollBar.size < 1.0) {
// If the scrollbar is required change it's color based on activity.
if (scrollBar.active) {
return RustColors.scrollbar_active;
} else {
return RustColors.scrollbar;
}
} else {
// If we don't need a scrollbar, fallback to the gutter color.
return gutter.color;
}
}
color: barColor
implicitHeight: scrollBar.h
implicitWidth: scrollBar.w
// Fade the scrollbar when inactive.
opacity: scrollBar.o
radius: 20
// Smooth transition between color changes based on the state above.
Behavior on color {
ColorAnimation {
duration: 1000
}
}
Behavior on opacity {
OpacityAnimator {
duration: 1000
}
}
}
}

9
qml/Components/qmldir Normal file
View File

@@ -0,0 +1,9 @@
ClideScrollBar ClideScrollBar.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

35
qml/Logger/Logger.qml Normal file
View File

@@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: 2026, Shaun Reed <shaunrd0@gmail.com>
//
// SPDX-License-Identifier: GNU General Public License v3.0 or later
pragma Singleton
import QtQuick
QtObject {
signal logged(string level, string message)
function debug(msg) {
console.log(msg);
logged("DEBUG", msg);
}
function 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);
}
function warn(msg) {
console.warn(msg);
logged("WARN", msg);
}
}

1
qml/Logger/qmldir Normal file
View File

@@ -0,0 +1 @@
singleton Logger 1.0 Logger.qml

View File

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

View File

@@ -1,5 +1,10 @@
<RCC> <RCC>
<qresource prefix="/images"> <qresource prefix="/images">
<file alias="kilroy.png">images/kilroy-256.png</file> <file alias="kilroy.png">resources/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.

Binary file not shown.

Binary file not shown.

BIN
resources/gui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -37,6 +37,11 @@ pub mod qobject {
#[qproperty(QColor, explorer_folder)] #[qproperty(QColor, explorer_folder)]
#[qproperty(QColor, explorer_folder_open)] #[qproperty(QColor, explorer_folder_open)]
#[qproperty(QColor, terminal_background)] #[qproperty(QColor, terminal_background)]
#[qproperty(QColor, info_log)]
#[qproperty(QColor, debug_log)]
#[qproperty(QColor, warn_log)]
#[qproperty(QColor, error_log)]
#[qproperty(QColor, trace_log)]
type RustColors = super::RustColorsImpl; type RustColors = super::RustColorsImpl;
} }
} }
@@ -67,6 +72,11 @@ pub struct RustColorsImpl {
explorer_folder: QColor, explorer_folder: QColor,
explorer_folder_open: QColor, explorer_folder_open: QColor,
terminal_background: QColor, terminal_background: QColor,
info_log: QColor,
debug_log: QColor,
warn_log: QColor,
error_log: QColor,
trace_log: QColor,
} }
impl Default for RustColorsImpl { impl Default for RustColorsImpl {
@@ -83,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("#2b2b2b").unwrap(), editor_background: QColor::try_from("#1E1F22").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(),
@@ -93,8 +103,13 @@ 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("#2b2b2b").unwrap(), explorer_folder_open: QColor::try_from("#393B40").unwrap(),
terminal_background: QColor::try_from("#2C2E32").unwrap(), terminal_background: QColor::try_from("#111111").unwrap(),
info_log: QColor::try_from("#C4FFFF").unwrap(),
debug_log: QColor::try_from("#9148AF").unwrap(),
warn_log: QColor::try_from("#C4A958").unwrap(),
error_log: QColor::try_from("#ff5555").unwrap(),
trace_log: QColor::try_from("#ffaa00").unwrap(),
} }
} }
} }

View File

@@ -3,15 +3,14 @@
// 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::{ use syntect::html::{IncludeBackground, append_highlighted_html_for_styled_line};
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;
@@ -52,10 +51,12 @@ 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,
} }
@@ -95,7 +96,8 @@ 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);
let (mut output, _bg) = start_highlighted_html_snippet(theme); // If you care about the background, see `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)
@@ -103,7 +105,7 @@ impl qobject::FileSystem {
append_highlighted_html_for_styled_line( append_highlighted_html_for_styled_line(
&regions[..], &regions[..],
IncludeBackground::Yes, IncludeBackground::No,
&mut output, &mut output,
) )
.expect("Failed to insert highlighted html"); .expect("Failed to insert highlighted html");
@@ -139,4 +141,15 @@ 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())
}
} }