From 7d9e81c60f28a37500ede7b6cfe42e3ab6946917 Mon Sep 17 00:00:00 2001 From: Qrakhen Date: Wed, 19 Nov 2025 20:47:03 +0100 Subject: [PATCH] editor caret works! (i should focus on the language, shouldnt i?) --- Qrakhen.Qamp.Editor/App.xaml.cs | 2 +- Qrakhen.Qamp.Editor/Commands/RelayCommand.cs | 30 +++++++ Qrakhen.Qamp.Editor/Controls/Caret.cs | 35 ++++++++ Qrakhen.Qamp.Editor/Controls/EditorFrame.xaml | 12 ++- .../Controls/EditorFrame.xaml.cs | 88 +++++++++++++++++-- .../Controls/LineRenderer.xaml | 2 +- .../Primitives/CursorPosition.cs | 22 +++++ .../Primitives/CursorRegion.cs | 7 ++ .../ViewModel/EditorFrameViewModel.cs | 72 +++++++++++++++ .../ViewModel/MainViewModel.cs | 37 +------- .../ViewModel/ObservableObject.cs | 25 ++++++ 11 files changed, 284 insertions(+), 48 deletions(-) create mode 100644 Qrakhen.Qamp.Editor/Commands/RelayCommand.cs create mode 100644 Qrakhen.Qamp.Editor/Controls/Caret.cs create mode 100644 Qrakhen.Qamp.Editor/Primitives/CursorPosition.cs create mode 100644 Qrakhen.Qamp.Editor/Primitives/CursorRegion.cs create mode 100644 Qrakhen.Qamp.Editor/ViewModel/EditorFrameViewModel.cs create mode 100644 Qrakhen.Qamp.Editor/ViewModel/ObservableObject.cs diff --git a/Qrakhen.Qamp.Editor/App.xaml.cs b/Qrakhen.Qamp.Editor/App.xaml.cs index 6046470..9e7fb82 100644 --- a/Qrakhen.Qamp.Editor/App.xaml.cs +++ b/Qrakhen.Qamp.Editor/App.xaml.cs @@ -11,7 +11,7 @@ public partial class App : Application protected override void OnStartup(StartupEventArgs e) { MainWindow window = new MainWindow() { - DataContext = new ViewModel.FileViewModel() + DataContext = new ViewModel.EditorFrameViewModel() }; window.Show(); diff --git a/Qrakhen.Qamp.Editor/Commands/RelayCommand.cs b/Qrakhen.Qamp.Editor/Commands/RelayCommand.cs new file mode 100644 index 0000000..f218422 --- /dev/null +++ b/Qrakhen.Qamp.Editor/Commands/RelayCommand.cs @@ -0,0 +1,30 @@ +using System.Windows.Input; + +namespace Qrakhen.Qamp.Editor.Commands; + +public class RelayCommand : ICommand +{ + private readonly Action _execute; + private readonly Func _canExecute; + + public RelayCommand(Action execute) : this(execute, null) { } + + public RelayCommand(Action execute, Func? canExecute) + { + _execute = execute ?? throw new ArgumentNullException(nameof(execute)); + _canExecute = canExecute ?? (p => true); + } + + public RelayCommand(Action execute) : this(p => execute()) { } + + public RelayCommand(Action execute, Func canExecute) : this(p => execute(), p => canExecute()) { } + + public event EventHandler? CanExecuteChanged { + add => CommandManager.RequerySuggested += value; + remove => CommandManager.RequerySuggested -= value; + } + + public bool CanExecute(object? parameter) => _canExecute(parameter); + + public void Execute(object? parameter) => _execute(parameter); +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Editor/Controls/Caret.cs b/Qrakhen.Qamp.Editor/Controls/Caret.cs new file mode 100644 index 0000000..7643fd9 --- /dev/null +++ b/Qrakhen.Qamp.Editor/Controls/Caret.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows; +using System.Windows.Documents; +using System.Windows.Media; + +namespace Qrakhen.Qamp.Editor.Controls; + +public class Caret : Adorner +{ + private readonly Pen _caretPen; + private readonly double _lineHeight; + private Point _cursorPosition; + + public Caret(UIElement adornedElement, double lineHeight) : base(adornedElement) + { + _lineHeight = lineHeight; + _caretPen = new Pen(Brushes.LimeGreen, 1.5); + IsHitTestVisible = false; + } + + public void UpdatePosition(Point newPosition) + { + _cursorPosition = newPosition; + InvalidateVisual(); + } + + protected override void OnRender(DrawingContext drawingContext) + { + Point startPoint = new(_cursorPosition.X, _cursorPosition.Y); + Point endPoint = new(_cursorPosition.X, _cursorPosition.Y + _lineHeight); + drawingContext.DrawLine(_caretPen, startPoint, endPoint); + } +} diff --git a/Qrakhen.Qamp.Editor/Controls/EditorFrame.xaml b/Qrakhen.Qamp.Editor/Controls/EditorFrame.xaml index b4cd9f4..295e860 100644 --- a/Qrakhen.Qamp.Editor/Controls/EditorFrame.xaml +++ b/Qrakhen.Qamp.Editor/Controls/EditorFrame.xaml @@ -6,9 +6,17 @@ xmlns:local="clr-namespace:Qrakhen.Qamp.Editor.Controls" mc:Ignorable="d" Background="#161718" + Focusable="True" d:DesignHeight="450" d:DesignWidth="800"> - - + + + + diff --git a/Qrakhen.Qamp.Editor/Controls/EditorFrame.xaml.cs b/Qrakhen.Qamp.Editor/Controls/EditorFrame.xaml.cs index 6b93768..5be395b 100644 --- a/Qrakhen.Qamp.Editor/Controls/EditorFrame.xaml.cs +++ b/Qrakhen.Qamp.Editor/Controls/EditorFrame.xaml.cs @@ -1,22 +1,94 @@ -using System; -using System.Collections.Generic; -using System.Text; +using Qrakhen.Qamp.Editor.Primitives; +using Qrakhen.Qamp.Editor.ViewModel; using System.Windows; using System.Windows.Controls; -using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; namespace Qrakhen.Qamp.Editor.Controls; public partial class EditorFrame : UserControl { + public Caret Caret { get; private set; } + public EditorFrame() { InitializeComponent(); + Loaded += OnLoaded; + PreviewKeyDown += OnKeyDown; + } + + private void OnKeyDown(object sender, KeyEventArgs e) + { + if (DataContext is EditorFrameViewModel vm) { + CursorPosition position = e.Key switch { + Key.Right => new(0, 1), + Key.Left => new(0, -1), + Key.Up => new(-1, 0), + Key.Down => new(1, 0), + _ => new(0, 0) + }; + if (vm.MoveCaretCommand.CanExecute(position)) { + vm.MoveCaretCommand.Execute(position); + e.Handled = true; + } + } + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + Focus(); + AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(this); + if (adornerLayer != null) { + Caret = new Caret(this, 18); + adornerLayer.Add(Caret); + } else { + System.Diagnostics.Debug.WriteLine("AdornerLayer still null. Check parent hierarchy."); + } + Loaded -= OnLoaded; + } + + private Point CalculateCaretPosition(int line, int index, double scrollY, double scrollX) + { + return new Point(10 * index + scrollX, line * 18 + scrollY); + } + + private void MoveCaret(Point point) + { + Caret?.UpdatePosition(point); + } + + private void UpdateCaret() + { + int line = CursorPosition.Line; + int charIndex = CursorPosition.Column; + + double scrollY = ScrollView.VerticalOffset; + double scrollX = ScrollView.HorizontalOffset; + + MoveCaret( + CalculateCaretPosition( + line, + charIndex, + scrollY, + scrollX)); + } + + public CursorPosition CursorPosition { + get => (CursorPosition)GetValue(CursorPositionProperty); + set => SetValue(CursorPositionProperty, value); + } + + public static readonly DependencyProperty CursorPositionProperty = + DependencyProperty.Register( + nameof(CursorPosition), + typeof(CursorPosition), + typeof(EditorFrame), + new PropertyMetadata(CursorPosition.Void, OnCursorPositionChanged)); + + private static void OnCursorPositionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var frame = (EditorFrame)d; + frame.UpdateCaret(); } } diff --git a/Qrakhen.Qamp.Editor/Controls/LineRenderer.xaml b/Qrakhen.Qamp.Editor/Controls/LineRenderer.xaml index dcdce90..b3c0f66 100644 --- a/Qrakhen.Qamp.Editor/Controls/LineRenderer.xaml +++ b/Qrakhen.Qamp.Editor/Controls/LineRenderer.xaml @@ -5,7 +5,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:Qrakhen.Qamp.Editor.Controls" mc:Ignorable="d" - d:DesignHeight="450" d:DesignWidth="800"> + d:DesignHeight="18" d:DesignWidth="420"> diff --git a/Qrakhen.Qamp.Editor/Primitives/CursorPosition.cs b/Qrakhen.Qamp.Editor/Primitives/CursorPosition.cs new file mode 100644 index 0000000..577c49b --- /dev/null +++ b/Qrakhen.Qamp.Editor/Primitives/CursorPosition.cs @@ -0,0 +1,22 @@ +namespace Qrakhen.Qamp.Editor.Primitives; + +public record struct CursorPosition(int line = 0, int column = 0) +{ + public static readonly CursorPosition Void = new(0, 0); + public static readonly CursorPosition Left = new(0, -1); + public static readonly CursorPosition Right = new(0, 1); + public static readonly CursorPosition Up = new(-1, 0); + public static readonly CursorPosition Down = new(1, 0); + + public int Line = line < 0 ? 0 : line; + public int Column = column < 0 ? 0 : column; + + public static CursorPosition operator +(CursorPosition left, CursorPosition right) + => new(left.Line + right.Line, left.Column + right.Column); + + public static CursorPosition operator -(CursorPosition left, CursorPosition right) + => new(left.Line - right.Line, left.Column - right.Column); + + public static CursorPosition operator *(CursorPosition left, int factor) + => new(left.Line * factor, left.Column * factor); +} diff --git a/Qrakhen.Qamp.Editor/Primitives/CursorRegion.cs b/Qrakhen.Qamp.Editor/Primitives/CursorRegion.cs new file mode 100644 index 0000000..ea70623 --- /dev/null +++ b/Qrakhen.Qamp.Editor/Primitives/CursorRegion.cs @@ -0,0 +1,7 @@ +namespace Qrakhen.Qamp.Editor.Primitives; + +public struct CursorRegion(CursorPosition from, CursorPosition to) +{ + public CursorPosition From = from; + public CursorPosition To = to; +} \ No newline at end of file diff --git a/Qrakhen.Qamp.Editor/ViewModel/EditorFrameViewModel.cs b/Qrakhen.Qamp.Editor/ViewModel/EditorFrameViewModel.cs new file mode 100644 index 0000000..0a566ba --- /dev/null +++ b/Qrakhen.Qamp.Editor/ViewModel/EditorFrameViewModel.cs @@ -0,0 +1,72 @@ +using Qrakhen.Qamp.Editor.Commands; +using Qrakhen.Qamp.Editor.Primitives; +using Qrakhen.Qamp.Memory; +using System.Collections.ObjectModel; +using System.Windows.Input; + +namespace Qrakhen.Qamp.Editor.ViewModel; + +public class EditorFrameViewModel : ObservableObject +{ + private CursorPosition _cursorPosition; + public CursorPosition CursorPosition { + get => _cursorPosition; + set => SetProperty(ref _cursorPosition, value); + } + + public int CursorLine { + get => _cursorPosition.Line; + set => SetProperty(ref _cursorPosition.Line, value); + } + + public int CursorColumn { + get => _cursorPosition.Column; + set => SetProperty(ref _cursorPosition.Column, value); + } + + public LineBuffer CurrentLineBuffer => Lines[CursorLine]; + + public ICommand MoveCaretCommand { get; } + + public ObservableCollection Lines { get; private set; } = new() { + new LineBuffer("test"), + new LineBuffer(" is very good;"), + new LineBuffer(" jaja();"), + new LineBuffer("}") + }; + + public EditorFrameViewModel() + { + MoveCaretCommand = new RelayCommand(ExecuteMoveCaret, CanMoveCaret); + } + + public void SetCursor(int line, int column) + { + line = line < 0 ? 0 : line >= Lines.Count ? Lines.Count - 1 : line; + LineBuffer buffer = Lines[line]; + column = column < 0 ? 0 : column > buffer.Tail ? buffer.Tail : column; + CursorPosition = new(line, column); + } + + public void MoveCursor(int lines, int columns) + { + SetCursor(CursorLine + lines, CursorColumn + columns); + } + + public bool CanMoveCaret(object? relativeUpdate) + { + return true; + } + + public void ExecuteMoveCaret(object? relativeUpdate) + { + if (relativeUpdate is CursorPosition pos) + MoveCursor(pos.Line, pos.Column); + } + + public void Insert(string data) + { + CurrentLineBuffer.Insert(CursorColumn, data); + OnPropertyChanged(nameof(Lines)); + } +} diff --git a/Qrakhen.Qamp.Editor/ViewModel/MainViewModel.cs b/Qrakhen.Qamp.Editor/ViewModel/MainViewModel.cs index 20219ca..a4c5ea5 100644 --- a/Qrakhen.Qamp.Editor/ViewModel/MainViewModel.cs +++ b/Qrakhen.Qamp.Editor/ViewModel/MainViewModel.cs @@ -1,10 +1,5 @@ -using Qrakhen.Qamp.Memory; -using System; +using System; using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; using System.Text; namespace Qrakhen.Qamp.Editor.ViewModel; @@ -13,33 +8,3 @@ public class MainViewModel { } - -public class FileViewModel : ObservableObject -{ - public ObservableCollection Lines { get; private set; } = new() { - new LineBuffer("test"), - new LineBuffer(" is very good;"), - new LineBuffer(" jaja();"), - new LineBuffer("}") - }; -} - -public abstract class ObservableObject : INotifyPropertyChanged -{ - public event PropertyChangedEventHandler? PropertyChanged; - - protected bool SetProperty([NotNullIfNotNull("newValue")] ref T field, T newValue, [CallerMemberName] string? propertyName = null) - { - if (EqualityComparer.Default.Equals(field, newValue)) - return false; - - field = newValue; - OnPropertyChanged(propertyName); - return true; - } - - protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } -} \ No newline at end of file diff --git a/Qrakhen.Qamp.Editor/ViewModel/ObservableObject.cs b/Qrakhen.Qamp.Editor/ViewModel/ObservableObject.cs new file mode 100644 index 0000000..ab680d5 --- /dev/null +++ b/Qrakhen.Qamp.Editor/ViewModel/ObservableObject.cs @@ -0,0 +1,25 @@ +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Qrakhen.Qamp.Editor.ViewModel; + +public abstract class ObservableObject : INotifyPropertyChanged +{ + public event PropertyChangedEventHandler? PropertyChanged; + + protected bool SetProperty([NotNullIfNotNull("newValue")] ref T field, T newValue, [CallerMemberName] string? propertyName = null) + { + if (EqualityComparer.Default.Equals(field, newValue)) + return false; + + field = newValue; + OnPropertyChanged(propertyName); + return true; + } + + protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} \ No newline at end of file