using System.Windows.Controls; namespace Qrakhen.TilingFrames.Models; /// /// This class represents a branch in the tree structure, having two children,
/// which can be either another - in the case of the tree branching onwards -
/// or a , if we're at the end of a branch (a so-called leaf node).
/// A panel contains information about how its children and /// are separated by exposing an and split ratio. ///
/// /// The entire attaching and detaching logic is handled from static methods in this class for brevity.
/// Those methods are and . ///
public class TilingPanel : TilingNode { /// private Orientation _splitOrientation = Orientation.Horizontal; /// /// The orientation of the split, so either vertical or horizontal. /// public Orientation SplitOrientation { get => _splitOrientation; set => SetField(ref _splitOrientation, value); } /// private double _splitRatio = 0.5; /// /// The width or height ratio of the split, with 0.5 being the exact middle. /// public double SplitRatio { get => _splitRatio; set => SetField(ref _splitRatio, Math.Min(1, Math.Max(0, value))); } /// private TilingNode _alpha; /// /// 'Alpha is always set' is an enforced paradigm, so it will always point to an existing node. /// public TilingNode Alpha { get => _alpha; private set => SetField(ref _alpha, value); } /// private TilingNode? _beta; /// /// The beta node may be null, for example if a panel is not yet split. /// public TilingNode? Beta { get => _beta; private set => SetField(ref _beta, value); } /// /// Whether this panel is split in two, so whether beta is not null. /// public bool IsSplit => Beta != null; public TilingPanel(TilingNode alpha, TilingNode? beta = null) : base() { _alpha = alpha; _beta = beta; } /// /// Traverses down the entire tree from until /// is encountered, returning its direct parent panel. /// /// /// This may not be the quickest approach, but we're looking at UI tiling panels with perhaps 20 children at most. /// public static TilingPanel? GetParentPanel(TilingPanel rootPanel, TilingNode node) { if (rootPanel.Alpha == node || rootPanel.Beta == node) { return rootPanel; } TilingPanel? parent = null; if (rootPanel.Alpha is TilingPanel alphaPanel) { parent = GetParentPanel(alphaPanel, node); } if (parent == null && rootPanel.Beta is TilingPanel betaPanel) { parent = GetParentPanel(betaPanel, node); } return parent; } /// /// /// Attaches 's active frame to 's parent as a new .
/// The parent will be looked up by traversing the tree from .
/// The following ordered rules apply when attaching: ///
/// /// 1. If 's parent has an open beta slot, will become the beta node.
/// 2. If one of the parent's child nodes is an , it will be replaced with
/// 3. If none of the above apply, the parent's beta node will be split into a new tiling panel. ///
///
/// /// All of these actions will automatically split the parent panel.
/// If had a parent assigned previously and only a single frame, it will be detached from that branch first.
/// If more frames are present in , only the active frame will be removed from it,
/// and a new is created, containing only that frame.
/// Note that all passed elements have to be children to the same tree. ///
/// The root to start branch traversal from. /// The host to be split. /// /// The host to be attached to 's parent. /// If set to null, an will be created in its place. /// /// The direction at which to drop relative to . /// The that now contains and . public static TilingPanel Attach(TilingPanel rootPanel, TilingHost targetHost, TilingHost? newHost, TilingDirection direction) { // This method looks way more complicated than it actually is, // all I wanted to achieve is one centralized place where branching and child assignments happen, // as tree structures are notoriously hard to navigate and handle when putting references everywhere. // It really boils down to a simple three-step process: // 1. Get or create correct host to attach, // 2. Find out where to place the new host (Alpha or Beta, depending on tiling direction), // 3. Create branch to be attached, or attach to free slot of parent. // Todo: This note is in place for the reviewer to have a bit more context. Remove when done. TilingPanel? parent = GetParentPanel(rootPanel, targetHost); if (parent == null) { throw new NullReferenceException($"Detached target: Could not find a parent for {targetHost} in any of {rootPanel}'s child nodes."); } if (newHost == null) { // Substitute for the split. newHost = new EmptyHost(); } else { if (newHost.Frames.Count < 2) { // Detach newHost from a potential parent. Detach(rootPanel, newHost); } else { // Only take the active frame from newHost and instantiate a host to attach. HostFrame frame = newHost.ActiveFrame!; // Frames.Count >= 2 already ensures ActiveFrame not to be null. newHost = new TilingHost([frame]); } } // I tried formulating an explanation for this step but I simply can't. // "Left or Top go alpha, Bottom or Right go beta." bool newHostIsAlpha = direction is TilingDirection.Left or TilingDirection.Top; // Check whether there's a free beta slot in the parent to use if (parent.Beta == null) { if (newHostIsAlpha) { // Open beta slot but newHost wants to be in alpha slot, simply swap and attach. parent.Beta = parent.Alpha; parent.Alpha = newHost; } else { // Open beta slot means we just put newHost there, no branching needed. parent.Beta = newHost; } return parent; } // Create a new branch to hold both target- and newHost. TilingPanel branch; if (direction is TilingDirection.Left or TilingDirection.Top) { branch = new TilingPanel(newHost, targetHost); } else { branch = new TilingPanel(targetHost, newHost); } // Assign new branch to correct chíld node. if (parent.Alpha == targetHost) { parent.Alpha = branch; } else if (parent.Beta == targetHost) { parent.Beta = branch; } return branch; } /// /// Detaches from its parent node, which will be looked up by traversing the .
/// If the child happened to be the alpha value, the beta value will be moved into the alpha slot to ensure alpha always having a value.
/// If the child's parent node ends up having no children left, which would violate the 'alpha is always set' rule,
/// that node itself will also be detached in order to prevent trailing zombie nodes. ///
public static void Detach(TilingPanel rootPanel, TilingNode child) { TilingPanel? parent = GetParentPanel(rootPanel, child); if (parent == null) { return; // Already an orphan. } if (parent.Alpha == child) { if (parent.Beta != null) { // Move beta over to alpha if beta has a value. parent.Alpha = parent.Beta; parent.Beta = null; } else { // Both branches have detached, which means we'll detach the parent itself // in order to prevent empty, trailing panels with no children. Detach(rootPanel, child); } } else { // Child was the beta node, so we simply null it. parent.Beta = null; } } }