21 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
30 changed files with 636 additions and 467 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"

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,16 +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/ClideLogger.qml",
"qml/Components/ClideScrollBar.qml",
"qml/Components/ClideHandle.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/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
}
}

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,191 +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 {
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
}
}
}

View File

@@ -1,112 +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
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,38 +10,30 @@ import clide.module 1.0
import Logger 1.0 import Logger 1.0
TreeView { TreeView {
id: fileSystemTreeView id: root
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
rootIndex: FileSystem.setDirectory(fileSystemTreeView.rootDirectory) // 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. // Provide our own custom ScrollIndicator for the TreeView.
ScrollIndicator.vertical: ScrollIndicator { ScrollBar.horizontal: ClideScrollBar {
active: true sizeModifier: 3
implicitWidth: 15 }
ScrollBar.vertical: ClideScrollBar {
contentItem: Rectangle { sizeModifier: 3
color: RustColors.scrollbar
implicitHeight: 6
implicitWidth: 6
opacity: fileSystemTreeView.movingVertically ? 0.5 : 0.0
Behavior on opacity {
OpacityAnimator {
duration: 500
}
}
}
} }
// The delegate represents a single entry in the filesystem. // The delegate represents a single entry in the filesystem.
@@ -53,68 +45,65 @@ TreeView {
required property int index required property int index
implicitHeight: 25 implicitHeight: 25
implicitWidth: fileSystemTreeView.width > 0 ? fileSystemTreeView.width : 250 implicitWidth: root.width
indentation: 12 indentation: 12
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) ? RustColors.explorer_text_selected : (hoverHandler.hovered ? RustColors.explorer_hovered : "transparent") radius: 20
opacity: hoverHandler.hovered ? 0.75 : 1.0 width: root.width
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
} }
indicator: Image { // Item Icon.
id: directoryIcon indicator: Label {
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
asynchronous: true enabled: false
fillMode: Image.PreserveAspectFit focus: false
font.family: localFont.font.family
font.pixelSize: 18
smooth: true smooth: true
source: setSourceImage() // Get the icon from Rust implementation.
sourceSize.height: 15 text: root.model.icon(filePath)
sourceSize.width: 15 x: root.rootIndent + (treeDelegate.depth * treeDelegate.indentation) + (carrotIndicator.visible ? carrotIndicator.width : 0)
x: treeDelegate.leftMargin + (treeDelegate.depth * treeDelegate.indentation)
} }
MultiEffect { // Directory carrot indicator.
id: iconOverlay Label {
id: carrotIndicator
anchors.fill: directoryIcon 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 {
anchors.fill: itemIcon
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: directoryIcon source: itemIcon
} }
HoverHandler { HoverHandler {
id: hoverHandler id: hoverHandler
@@ -127,11 +116,12 @@ TreeView {
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();
@@ -139,26 +129,38 @@ TreeView {
} }
} }
} }
Menu { ClideMenu {
id: contextMenu id: contextMenu
Action { ClideMenuItem {
enabled: treeDelegate.hasChildren action: Action {
text: qsTr("Set as root index") enabled: treeDelegate.hasChildren
text: qsTr("Set root")
onTriggered: { onTriggered: {
Logger.debug("Setting new root directory: " + treeDelegate.filePath); Logger.debug("Setting new root directory: " + treeDelegate.filePath);
fileSystemTreeView.rootDirectory = treeDelegate.filePath; root.rootDirectory = treeDelegate.filePath;
}
} }
} }
Action { ClideMenuItem {
text: qsTr("Reset root index") action: Action {
text: qsTr("Reset root")
onTriggered: { onTriggered: {
Logger.log("Resetting root directory: " + fileSystemTreeView.originalRootDirectory); Logger.log("Resetting root directory: " + root.originalRootDirectory);
fileSystemTreeView.rootDirectory = fileSystemTreeView.originalRootDirectory; root.rootDirectory = root.originalRootDirectory;
}
} }
} }
} }
} }
selectionModel: ItemSelectionModel {
}
FontLoader {
id: localFont
source: "qrc:/fonts/saucecodepro-xlight.ttf"
}
} }

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

@@ -9,34 +9,12 @@ import QtQuick.Layouts
import clide.module 1.0 import clide.module 1.0
import Logger 1.0 import Logger 1.0
SplitView { Rectangle {
id: root color: RustColors.editor_background
// 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.
@@ -84,12 +62,8 @@ SplitView {
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 edge along the right side of the line number. // Draw an edge along the right side of the line number.
Rectangle { Rectangle {
id: indicator id: indicator
@@ -130,28 +104,6 @@ SplitView {
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 {
@@ -161,8 +113,4 @@ SplitView {
} }
} }
} }
ClideLogger {
id: areaConsole
}
} }

View File

@@ -8,13 +8,15 @@ 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 RustColors.gutter; return "transparent";
} }
} }
required property bool hovered required property bool hovered
@@ -23,12 +25,20 @@ 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: 400 duration: 500
}
}
Behavior on opacity {
OpacityAnimator {
duration: 500
} }
} }
} }

View File

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

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

@@ -11,9 +11,10 @@ ScrollBar {
id: scrollBar id: scrollBar
// Height, opacitiy, width // Height, opacitiy, width
property int h: scrollBar.interactive ? 4 * 2 : 4 property int h: scrollBar.interactive ? sizeModifier * 2 : sizeModifier
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 w: scrollBar.interactive ? 4 * 2 : 4 property int sizeModifier: 4
property int w: scrollBar.interactive ? sizeModifier * 2 : sizeModifier
// Scroll bar gutter // Scroll bar gutter
background: Rectangle { background: Rectangle {
@@ -25,6 +26,7 @@ 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 {
@@ -55,6 +57,7 @@ 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,2 +1,9 @@
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,22 +15,29 @@ 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 {
} }
MessageDialog { Rectangle {
id: errorDialog color: RustColors.menubar
width: appView.implicitWidth
height: appView.implicitHeight
title: qsTr("Error") ClideApplicationView {
} id: appView
ClideProjectView { projectDir: appWindow.appContextPath
id: clideProjectView implicitHeight: appWindow.height
implicitWidth: appWindow.width
projectDir: appWindow.appContextPath 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

@@ -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("#111111").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(),
@@ -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("#2b2b2b").unwrap(), explorer_folder_open: QColor::try_from("#393B40").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,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())
}
} }