5 Commits

Author SHA1 Message Date
1b60947177 WIP 2026-02-07 21:30:26 -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
17 changed files with 302 additions and 176 deletions

10
Cargo.lock generated
View File

@@ -295,6 +295,7 @@ dependencies = [
"cxx-qt",
"cxx-qt-build",
"cxx-qt-lib",
"devicons",
"dirs",
"edtui",
"log",
@@ -667,6 +668,15 @@ dependencies = [
"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]]
name = "digest"
version = "0.10.7"

View File

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

View File

@@ -12,6 +12,9 @@ fn main() {
"qml/ClideLogger.qml",
"qml/Components/ClideScrollBar.qml",
"qml/Components/ClideHandle.qml",
"qml/Components/ClideMenu.qml",
"qml/Components/ClideMenuItem.qml",
"qml/Components/ClideBreadCrumbs.qml",
"qml/Logger/Logger.qml",
]))
// Link Qt's Network library

View File

@@ -10,128 +10,116 @@ 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
action: Action {
id: actionNewProject
text: qsTr("&New Project...")
}
}
ClideMenuItem {
action: actionOpen
action: Action {
id: actionOpen
text: qsTr("&Open...")
}
onTriggered: FileSystem.setDirectory(FileSystem.filePath)
}
ClideMenuItem {
action: actionSave
action: Action {
id: actionSave
text: qsTr("&Save")
}
}
MenuSeparator {
background: Rectangle {
border.color: color
color: RustColors.explorer_background
color: Qt.darker(RustColors.menubar, 1)
implicitHeight: 3
implicitWidth: 200
}
}
ClideMenuItem {
action: actionExit
action: Action {
id: actionExit
text: qsTr("&Exit")
onTriggered: Qt.quit()
}
}
}
//
// 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
action: Action {
id: actionUndo
text: qsTr("&Undo")
}
}
ClideMenuItem {
action: actionRedo
action: Action {
id: actionRedo
text: qsTr("&Redo")
}
}
ClideMenuItem {
action: actionCut
action: Action {
id: actionCut
text: qsTr("&Cut")
}
}
ClideMenuItem {
action: actionCopy
action: Action {
id: actionCopy
text: qsTr("&Copy")
}
}
ClideMenuItem {
action: actionPaste
action: Action {
id: actionPaste
text: qsTr("&Paste")
}
}
}
//
// View Menu
Action {
id: actionToolWindows
text: qsTr("&Tool Windows")
}
Action {
id: actionAppearance
text: qsTr("&Appearance")
}
ClideMenu {
title: qsTr("&View")
ClideMenuItem {
action: actionToolWindows
action: Action {
id: actionAppearance
text: qsTr("&Appearance")
}
}
ClideMenuItem {
action: actionAppearance
action: Action {
id: actionToolWindows
text: qsTr("&Tool Windows")
}
}
}
@@ -141,51 +129,25 @@ MenuBar {
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
action: Action {
id: actionDocumentation
text: qsTr("&Documentation")
}
}
ClideMenuItem {
action: actionAbout
}
}
action: Action {
id: actionAbout
// Base settings for each Menu.
component ClideMenu: Menu {
background: Rectangle {
color: RustColors.explorer_background
implicitWidth: 100
radius: 2
}
}
text: qsTr("&About")
// 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
// Toggle the about window with the menu item is clicked.
onTriggered: clideAbout.visible = !clideAbout.visible
}
}
}
}

View File

@@ -48,38 +48,17 @@ SplitView {
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
ClideBreadCrumbs {
id: breadCrumb
Text {
id: breadCrumb
Layout.bottomMargin: 5
Layout.leftMargin: 15
Layout.topMargin: 5
path: clideTreeView.rootDirectory
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;
}
}
onCrumbClicked: path => {
Logger.trace("Crumb clicked: " + path);
clideTreeView.rootDirectory = path;
}
}
ClideTreeView {
@@ -95,7 +74,7 @@ SplitView {
onFileClicked: path => clideEditor.filePath = path
onRootDirectoryChanged: {
Logger.log("Setting root directory: " + clideTreeView.rootDirectory);
breadCrumb.text = clideTreeView.rootDirectory;
breadCrumb.path = clideTreeView.rootDirectory;
}
}
}

View File

@@ -21,7 +21,7 @@ TreeView {
boundsBehavior: Flickable.StopAtBounds
boundsMovement: Flickable.StopAtBounds
clip: true
leftMargin: 5
leftMargin: 25
model: FileSystem
rootIndex: FileSystem.setDirectory(fileSystemTreeView.rootDirectory)
@@ -57,38 +57,43 @@ TreeView {
indentation: 12
background: Rectangle {
color: current ? RustColors.explorer_text_selected : "transparent"
color: current ? RustColors.explorer_folder_open : "transparent"
radius: 2.5
}
contentItem: Text {
anchors.left: directoryIcon.right
anchors.leftMargin: 5
color: RustColors.explorer_text
text: treeDelegate.fileName
}
indicator: Image {
indicator: Label {
id: directoryIcon
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
antialiasing: true
asynchronous: true
fillMode: Image.PreserveAspectFit
font.family: localFont.font.family
font.pixelSize: 18
smooth: true
source: setSourceImage()
sourceSize.height: 15
sourceSize.width: 15
x: treeDelegate.leftMargin + (treeDelegate.depth * treeDelegate.indentation)
text: fileSystemTreeView.model.icon(filePath)
x: treeDelegate.leftMargin + (treeDelegate.depth * treeDelegate.indentation) + (indicator.visible ? indicator.width : 0)
}
FontLoader {
id: localFont
source: "qrc:/fonts/saucecodepro-xlight.ttf"
}
Label {
id: indicator
anchors.verticalCenter: parent.verticalCenter
font.family: localFont.font.family
font.pixelSize: 10
font.weight: localFont.font.weight
text: expanded ? "⮟" : "⮞"
visible: isTreeNode && hasChildren
x: padding + (depth * indentation)
}
MultiEffect {
id: iconOverlay
@@ -120,10 +125,8 @@ TreeView {
case Qt.LeftButton:
if (treeDelegate.hasChildren) {
fileSystemTreeView.toggleExpanded(treeDelegate.row);
// fileSystemTreeView.lastIndex = treeDelegate.index
} else {
// If this model item doesn't have children, it means it's
// representing a file.
// If this model item doesn't have children, it means it's representing a file.
fileSystemTreeView.fileClicked(treeDelegate.filePath);
}
break;
@@ -133,24 +136,28 @@ TreeView {
}
}
}
Menu {
ClideMenu {
id: contextMenu
Action {
enabled: treeDelegate.hasChildren
text: qsTr("Set as root index")
ClideMenuItem {
action: Action {
enabled: treeDelegate.hasChildren
text: qsTr("Set root")
onTriggered: {
Logger.debug("Setting new root directory: " + treeDelegate.filePath);
fileSystemTreeView.rootDirectory = treeDelegate.filePath;
onTriggered: {
Logger.debug("Setting new root directory: " + treeDelegate.filePath);
fileSystemTreeView.rootDirectory = treeDelegate.filePath;
}
}
}
Action {
text: qsTr("Reset root index")
ClideMenuItem {
action: Action {
text: qsTr("Reset root")
onTriggered: {
Logger.log("Resetting root directory: " + fileSystemTreeView.originalRootDirectory);
fileSystemTreeView.rootDirectory = fileSystemTreeView.originalRootDirectory;
onTriggered: {
Logger.log("Resetting root directory: " + fileSystemTreeView.originalRootDirectory);
fileSystemTreeView.rootDirectory = fileSystemTreeView.originalRootDirectory;
}
}
}
}

View File

@@ -0,0 +1,109 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import clide.module 1.0
import Logger 1.0
Item {
id: root
property var fullPaths: []
required property string path
property var segments: []
signal crumbClicked(string path)
function rebuildSegments() {
var cleaned = path;
if (cleaned.endsWith("/"))
cleaned = cleaned.slice(0, -1);
var parts = cleaned.split("/");
root.segments = [];
root.fullPaths = [];
var current = "";
Logger.trace("Building segments for path: " + cleaned);
for (var i = 0; i < parts.length; ++i) {
if (parts[i] === "") {
current = "/";
root.segments.push("/");
root.fullPaths.push("/");
} else {
if (current === "/")
current += parts[i];
else
current += "/" + parts[i];
Logger.trace("Pushing path: " + parts[i] + " Current: " + current);
root.segments.push(parts[i]);
root.fullPaths.push(current);
}
}
rep.model = root.segments;
}
anchors.leftMargin: 20
height: breadcrumbRow.implicitHeight
width: parent.width
Component.onCompleted: rebuildSegments()
onPathChanged: rebuildSegments()
Flow {
id: breadcrumbRow
Repeater {
id: rep
model: root.segments
delegate: Text {
id: linkText
required property string modelData
function getText() {
if (modelData === "/") {
return modelData;
}
Logger.trace("Getting valid text:" + modelData);
return modelData + "/";
}
color: mouseArea.containsMouse ? "#2a7fff" : RustColors.explorer_text
font.underline: mouseArea.containsMouse
text: getText()
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
console.log("Breadcrumb clicked:", root.fullPaths[root.segments.indexOf(modelData)]);
root.crumbClicked(root.fullPaths[root.segments.indexOf(modelData)]);
}
}
}
}
}
TapHandler {
acceptedButtons: Qt.RightButton
onSingleTapped: (eventPoint, button) => contextMenu.popup()
}
ClideMenu {
id: contextMenu
ClideMenuItem {
action: Action {
text: qsTr("Reset root")
onTriggered: {
Logger.log("Resetting root directory: " + clideTreeView.originalRootDirectory);
clideTreeView.rootDirectory = clideTreeView.originalRootDirectory;
}
}
}
}
}

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,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

@@ -1,2 +1,5 @@
ClideScrollBar ClideScrollBar.qml
ClideHandle ClideHandle.qml
ClideMenu ClideMenu.qml
ClideMenuItem ClideMenuItem.qml
ClideBreadCrumbs ClideBreadCrumbs.qml

View File

@@ -1,5 +1,10 @@
<RCC>
<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>
</RCC>

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -103,7 +103,7 @@ impl Default for RustColorsImpl {
explorer_text_selected: QColor::try_from("#262626").unwrap(),
explorer_background: QColor::try_from("#1E1F22").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(),
info_log: QColor::try_from("#C4FFFF").unwrap(),
debug_log: QColor::try_from("#9148AF").unwrap(),

View File

@@ -3,6 +3,7 @@
// SPDX-License-Identifier: GNU General Public License v3.0 or later
use cxx_qt_lib::{QModelIndex, QString};
use devicons::FileIcon;
use dirs;
use log::warn;
use std::fs;
@@ -52,6 +53,9 @@ pub mod qobject {
#[qinvokable]
#[cxx_name = "setDirectory"]
fn set_directory(self: Pin<&mut FileSystem>, path: &QString) -> QModelIndex;
#[qinvokable]
fn icon(self: Pin<&mut FileSystem>, path: &QString) -> QString;
}
}
@@ -103,7 +107,7 @@ impl qobject::FileSystem {
append_highlighted_html_for_styled_line(
&regions[..],
IncludeBackground::Yes,
IncludeBackground::No,
&mut output,
)
.expect("Failed to insert highlighted html");
@@ -139,4 +143,15 @@ impl qobject::FileSystem {
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())
}
}