From bdf942371c9b5e044975922baa1691226b7872ae Mon Sep 17 00:00:00 2001 From: Shaun Reed Date: Sun, 30 Mar 2025 21:38:57 -0400 Subject: [PATCH] Add basic FileSystem view. --- Cargo.toml | 2 +- README.md | 2 +- build.rs | 3 ++ qml/ClideMenuBar.qml | 1 + qml/ClideProjectView.qml | 15 ++------ qml/ClideTreeView.qml | 74 +++++++++++++++++++--------------------- src/colors.rs | 11 +++++- src/filesystem.rs | 36 +++++++++++++------ 8 files changed, 80 insertions(+), 64 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c039058..91334c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" [dependencies] cxx = "1.0.95" cxx-qt = "0.7" -cxx-qt-lib = { version="0.7", features = ["qt_full"] } +cxx-qt-lib = { version="0.7", features = ["qt_full", "qt_gui"] } log = { version = "0.4.27", features = [] } dirs = "6.0.0" diff --git a/README.md b/README.md index 0caa5c2..ad8d381 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ CLIDE is an IDE written in Rust that supports both full and headless Linux envir The following packages must be installed before the application will build. ```bash -sudo apt install qt6-base-dev qt6-declarative-dev qt6-tools-dev qml6-module-qtquick-controls qml6-module-qtquick-layouts qml6-module-qtquick-window qml6-module-qtqml-workerscript qml6-module-qtquick-templates qml6-module-qtquick qml6-module-qtquick-dialogs +sudo apt install qt6-base-dev qt6-declarative-dev qt6-tools-dev qml6-module-qtquick-controls qml6-module-qtquick-layouts qml6-module-qtquick-window qml6-module-qtqml-workerscript qml6-module-qtquick-templates qml6-module-qtquick qml6-module-qtquick-dialogs qt6-svg-dev ``` And of course, [Rust](https://www.rust-lang.org/tools/install). diff --git a/build.rs b/build.rs index 69a103e..1e369a2 100644 --- a/build.rs +++ b/build.rs @@ -8,6 +8,9 @@ fn main() { // - Qt Qml is linked by enabling the qt_qml Cargo feature of cxx-qt-lib. // - Qt Qml requires linking Qt Network on macOS .qt_module("Network") + .qt_module("Gui") + .qt_module("Svg") + .qt_module("Xml") .qml_module(QmlModule { uri: "clide.module", rust_files: &["src/colors.rs", "src/filesystem.rs"], diff --git a/qml/ClideMenuBar.qml b/qml/ClideMenuBar.qml index 9a9661c..73a6655 100644 --- a/qml/ClideMenuBar.qml +++ b/qml/ClideMenuBar.qml @@ -67,6 +67,7 @@ MenuBar { } ClideMenuItem { action: actionOpen + onTriggered: FileSystem.setDirectory(FileSystem.filePath) } ClideMenuItem { action: actionSave diff --git a/qml/ClideProjectView.qml b/qml/ClideProjectView.qml index 569a038..fa1cad5 100644 --- a/qml/ClideProjectView.qml +++ b/qml/ClideProjectView.qml @@ -8,7 +8,7 @@ SplitView { id: root // Path to the file selected in the tree view. - property string selectedFilePath; + default property string selectedFilePath: FileSystem.filePath; Layout.fillHeight: true Layout.fillWidth: true @@ -38,23 +38,14 @@ SplitView { StackLayout { anchors.fill: parent - - // Shows the help text. - TextArea { - placeholderText: qsTr("File system view placeholder") - placeholderTextColor: "white" - readOnly: true - wrapMode: TextArea.Wrap - } - ClideTreeView { id: clideTreeView - onFileClicked: path => root.currentFilePath = path + onFileClicked: path => root.selectedFilePath = path } } } ClideEditor { // Initialize using the Default trait in Rust QML singleton FileSystem. - filePath: FileSystem.filePath + filePath: root.selectedFilePath } } diff --git a/qml/ClideTreeView.qml b/qml/ClideTreeView.qml index a2cf773..eeb05aa 100644 --- a/qml/ClideTreeView.qml +++ b/qml/ClideTreeView.qml @@ -1,25 +1,32 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import clide.module 1.0 Rectangle { id: root + signal fileClicked(string filePath) + color: RustColors.explorer_background + TreeView { id: fileSystemTreeView // rootIndex: FileSystem.rootIndex property int lastIndex: -1 - // model: FileSystem + model: FileSystem anchors.fill: parent boundsBehavior: Flickable.StopAtBounds boundsMovement: Flickable.StopAtBounds clip: true - Component.onCompleted: fileSystemTreeView.toggleExpanded(0) + Component.onCompleted: { + FileSystem.setDirectory(FileSystem.filePath) + fileSystemTreeView.expandRecursively(0, 4) + } // The delegate represents a single entry in the filesystem. delegate: TreeViewDelegate { @@ -28,9 +35,6 @@ Rectangle { implicitWidth: fileSystemTreeView.width > 0 ? fileSystemTreeView.width : 250 implicitHeight: 25 - // Since we have the 'ComponentBehavior Bound' pragma, we need to - // require these properties from our model. This is a convenient way - // to bind the properties provided by the model's role names. required property int index required property url filePath required property string fileName @@ -40,11 +44,16 @@ Rectangle { x: treeDelegate.leftMargin + (treeDelegate.depth * treeDelegate.indentation) anchors.verticalCenter: parent.verticalCenter - source: treeDelegate.hasChildren ? (treeDelegate.expanded - ? "../icons/folder_open.svg" : "../icons/folder_closed.svg") - : "../icons/generic_file.svg" - sourceSize.width: 20 - sourceSize.height: 20 + source: { + // If the item has children, it's a directory. + if (treeDelegate.hasChildren) { + return treeDelegate.expanded ? + "../icons/folder-open-solid.svg" : "../icons/folder-solid.svg"; + } + return "../icons/file-solid.svg" + } + sourceSize.width: 15 + sourceSize.height: 15 fillMode: Image.PreserveAspectFit smooth: true @@ -54,37 +63,23 @@ Rectangle { contentItem: Text { text: treeDelegate.fileName - color: RustColors.editor_text + color: RustColors.explorer_text } background: Rectangle { + // TODO: Fix flickering from color transition on states here. color: (treeDelegate.index === fileSystemTreeView.lastIndex) - ? RustColors.editor_highlight - : (hoverHandler.hovered ? RustColors.active : "transparent") - } + ? RustColors.explorer_text_selected + : (hoverHandler.hovered ? RustColors.explorer_hovered : "transparent") + radius: 2.5 + opacity: hoverHandler.hovered ? 0.75 : 1.0 - // We color the directory icons with this MultiEffect, where we overlay - // the colorization color ontop of the SVG icons. - // MultiEffect { - // id: iconOverlay - // - // anchors.fill: directoryIcon - // source: directoryIcon - // colorization: 1.0 - // brightness: 1.0 - // colorizationColor: { - // const isFile = treeDelegate.index === fileSystemTreeView.lastIndex - // && !treeDelegate.hasChildren; - // if (isFile) - // return Qt.lighter(RustColors.explorer_folder, 3) - // - // const isExpandedFolder = treeDelegate.expanded && treeDelegate.hasChildren; - // if (isExpandedFolder) - // return RustColors.explorer_forder_open - // else - // return RustColors.explorer_folder - // } - // } + Behavior on color { + ColorAnimation { + duration: 300 + } + } + } HoverHandler { id: hoverHandler @@ -115,12 +110,15 @@ Rectangle { Action { text: qsTr("Set as root index") onTriggered: { - // fileSystemTreeView.rootIndex = fileSystemTreeView.index(treeDelegate.row, 0) + console.log("Setting directory: " + treeDelegate.filePath) + FileSystem.setDirectory(treeDelegate.filePath) } } Action { text: qsTr("Reset root index") - // onTriggered: fileSystemTreeView.rootIndex = undefined + onTriggered: { + FileSystem.setDirectory("") + } } } } diff --git a/src/colors.rs b/src/colors.rs index 7d1f08f..cc10722 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -25,6 +25,9 @@ pub mod qobject { #[qproperty(QColor, editor_highlighted_text)] #[qproperty(QColor, editor_highlight)] #[qproperty(QColor, gutter)] + #[qproperty(QColor, explorer_hovered)] + #[qproperty(QColor, explorer_text)] + #[qproperty(QColor, explorer_text_selected)] #[qproperty(QColor, explorer_background)] #[qproperty(QColor, explorer_folder)] #[qproperty(QColor, explorer_folder_open)] @@ -51,6 +54,9 @@ pub struct RustColorsImpl { editor_highlighted_text: QColor, editor_highlight: QColor, gutter: QColor, + explorer_hovered: QColor, + explorer_text: QColor, + explorer_text_selected: QColor, explorer_background: QColor, explorer_folder: QColor, explorer_folder_open: QColor, @@ -75,7 +81,10 @@ impl Default for RustColorsImpl { editor_highlighted_text: QColor::try_from("#ccced3").unwrap(), editor_highlight: QColor::try_from("#ccced3").unwrap(), gutter: QColor::try_from("#1e1f22").unwrap(), - explorer_background: QColor::try_from("#3c3f41").unwrap(), + explorer_hovered: QColor::try_from("#4c5053").unwrap(), + explorer_text: QColor::try_from("#3b3b3b").unwrap(), + explorer_text_selected: QColor::try_from("#8b8b8b").unwrap(), + explorer_background: QColor::try_from("#676c70").unwrap(), explorer_folder: QColor::try_from("#FFF").unwrap(), explorer_folder_open: QColor::try_from("#FFF").unwrap(), } diff --git a/src/filesystem.rs b/src/filesystem.rs index 0f97344..540bcbe 100644 --- a/src/filesystem.rs +++ b/src/filesystem.rs @@ -6,29 +6,36 @@ pub mod qobject { type QString = cxx_qt_lib::QString; include!("cxx-qt-lib/qmodelindex.h"); type QModelIndex = cxx_qt_lib::QModelIndex; + include!(); + type QFileSystemModel; } unsafe extern "RustQt" { // Export QML Types from Rust #[qobject] + #[base = QFileSystemModel] #[qml_element] #[qml_singleton] #[qproperty(QString, file_path, cxx_name = "filePath")] #[qproperty(QModelIndex, root_index, cxx_name = "rootIndex")] type FileSystem = super::FileSystemImpl; + #[inherit] + #[cxx_name = "setRootPath"] + fn set_root_path(self: Pin<&mut FileSystem>, path: &QString) -> QModelIndex; + #[qinvokable] + #[cxx_override] #[cxx_name = "columnCount"] - pub fn column_count(self: &FileSystem, index: &QModelIndex) -> i32; + fn column_count(self: &FileSystem, _index: &QModelIndex) -> i32; #[qinvokable] #[cxx_name = "readFile"] fn read_file(self: &FileSystem, path: &QString) -> QString; - // TODO: Remove if unused in QML. #[qinvokable] - #[cxx_name = "setInitialDirectory"] - fn set_initial_directory(self: &FileSystem, path: &QString); + #[cxx_name = "setDirectory"] + fn set_directory(self: Pin<&mut FileSystem>, path: &QString) -> QModelIndex; } } @@ -36,6 +43,7 @@ use cxx_qt_lib::{QModelIndex, QString}; use dirs; use std::fs; +// TODO: Impleent a provider for QFileSystemModel::setIconProvider for icons. pub struct FileSystemImpl { file_path: QString, root_index: QModelIndex, @@ -64,21 +72,27 @@ impl qobject::FileSystem { } // There will never be more than one column. - pub fn column_count(&self, _index: &QModelIndex) -> i32 { + fn column_count(&self, _index: &QModelIndex) -> i32 { 1 } - fn set_initial_directory(&self, path: &QString) { + fn set_directory(self: std::pin::Pin<&mut Self>, path: &QString) -> QModelIndex { if !path.is_empty() && fs::metadata(path.to_string()) - .expect(format!("Failed to get metadata for file {}", path).as_str()) - .is_file() + .expect(format!("Failed to get metadata for path {}", path).as_str()) + .is_dir() { - // Open the file - // setRootPa + self.set_root_path(path) } else { // If the initial directory can't be opened, attempt to find the home directory. - // dirs::home_dir() + self.set_root_path(&QString::from( + dirs::home_dir() + .expect("Failed to get home directory") + .as_path() + .to_str() + .unwrap() + .to_string(), + )) } } }