Implementing Floating Panes and a Buffer Manager in my Text Editor (Yonro) — In form of a whimsy…
First lets see how the current architecture of the text editor looks like. I’d like to introduce it’s name: Yonro.. Yes it is a reference…
Implementing Floating Panes and a Buffer Manager in my Text Editor (Yonro) — In form of a whimsy tale
First lets see how the current architecture of the text editor looks like. I’d like to introduce it’s name: Yonro.. Yes it is a reference to something, a character I have been relating to a lot these days. But without further ado, let us start the tale of a harrowing adventure. I hope you will enjoy it.
For more context read the previous Blog : https://medium.com/@pumkininriver/pane-abstraction-in-thetext-editor-rust-that-i-am-building-to-use-as-writer-f4296c77372f?sharedUserId=pumkininriver
Terminal Input
│ crossterm::read()
▼
Event ──► Editor::evaluate_event() ──► Command
│
▼
Editor::process_command()
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
prompt_type? Resize (handled no prompt →
Search / Save immediately) dispatch by
FocusPane/Close Command variant
│ │ │
└─────────────────┴─────────────────┘
▼
Buffer / Pane / View updated
│
▼
Editor::refresh_screen() (every tick)
│
▼
Terminal Output
Floating Panes:
One day, Manglu was trying to jot down his thoughts, something he very rarely does, But as he was writing, more thoughts kept coming to him, he kept splitting more and more panes- some vertical, some horizontal. And he kept getting frustrated at himself, He needed a central Pane above the current panes which he could use to jot down all the main ideas that he wants to work on so that he can finally stop getting distracted and work on them one by one. He delegated this task to Yonro, to evolve itself to meet his needs. Now Yonro at the end of the day is just a Program which runs on an OS. It is not yet at the stage where it can change it’s nature by itself, with own introspection. It needs external help, more experinces in the world. okay okay I will get to the point. So I had to entertain Manglu’s request. How can I create a place on top of the current Tiled panes where he can write down the major different streams his thoughts are flowing in. The answer as you can guess is just create a floating Pane and dynamically dispatch the TextView into it. But now, How does it integrates into our Layout tree, for the 2D view, The Binary tree of Split and Leaf was sufficient as we were only concerned with how the size of rects change. But if we insert another parameter or action that can happen, Z index how does it change, and on it’s change, how does the tree needs to adjust itself? How do we know where do we need to render what?
- See in the 2D plane it was simple we know that this pane with a rect inside needs to be rendered according to the values assigned by the Layout tree. So there was no overlap of two panes. But with the introduction of this Z-index we need to consider the overlapping Panes, and how should we actually render them? Again let’s explore this problem from first principles and try to reach a working solution.
- One thing we already have clear is for Manglu’s request is that we just need to create a floating Pane with a Text View inside. The problem now is how to manage the rendering. So, we give the pane two more properties.
pub struct Pane {
pub pane_id: usize,
pub content: PaneContent,
pub active: bool,
// is the pane currently floating
pub is_floating: bool,
pub z_index: usize, // obviously Z-index
pub is_minimized: bool,// do not focus on this as of now
pub rect: Rect,
}
Okay, so by default these will have a value of false and 0 in the beginning. which will just create the previous flow of a Rect based rendering by the Layout tree. Now, How do we make use of this Z- index? In the previous complicated approach we were exploring the modification of Layout tree somehow. Now lets take a step back from there. Assume we have a list of all the panes, which all have a is_floating = false and z_index =0. We have the correct flow of rendering them based on their rect sizes. Now we will add a pane with is_floating= true and z_index = 1. Now what changes? we need to overlay this Pane on the previous rendered Grid or panes/pane. So first we render that z=0 and then tackle the z=1, separately. See we did not even interact with the Layout Tree at all. So we could handle this rendering for all the z indexes separately. Take a look at the current render flow:
LayoutTree (leaves) ──► PaneManager.get_pane_mut(id) ──► pane.render()
Now following the, previous practices in our code, we will delegate the creation and collection of these panes to Pane Manager without even touching the layout tree. Now Rendering as we all know is handled by the view itself, we just need to give it the correct Rect values.
// Code from fn refresh_screen()
if height > 2 {
// Tiled panes (layer 0)
for (pane_id, _) in self.layout_tree.collect_leaf_layouts() {
if let Some(pane) = self.pane_manager.get_pane_mut(pane_id) {
if !pane.is_floating {
pane.render(&self.buffer_manager);
}
}
}
// Floating panes sorted by z-index (layer 10+)
let floating_ids: Vec<usize> = self
.pane_manager
.get_floating_panes_sorted()
.iter()
.map(|p| p.pane_id)
.collect();
for id in floating_ids {
if let Some(pane) = self.pane_manager.get_pane_mut(id) {
pane.render(&self.buffer_manager);
}
}
}
Okay so did this:
- Render the tiled Panes first
- Render the floating panes in a sequence, so that the overlapping can happen correctly. But we have 2 questions Ahead of us now:
- How do we un float a pane and integrate it back into the Tiled Layout?
- How do we come back to this Pane when we focused on another pane? (I am not mentioning the resizing here, cause that flow is simple and you can check that out in code)
- How to make the floating panes Draggable?
So let’s tackle these questions one by one:
How do we un float a pane and integrate it back into the Tiled Layout?
let’s understand it with code now :
pub fn unfloat_pane(id: usize, ctx: &mut EditorContext) {
// ignore the ctx for now it will be explained in future blogs
// check if the current pane is floating or not
let is_floating = ctx
.pane_manager
.get_pane(id)
.map_or(false, |p| p.is_floating);
if !is_floating {
ctx.update_message("Pane is already tiled.");
return;
}
// while unfloating we need to merge it with a currently existing Pane
// for now I just merge it with the first pane_id we get maybe I will
// create complex flow in future
let target_id = ctx
.layout_tree
.collect_leaf_layouts()
.first()
.map(|(id, _)| *id);
match target_id {
None => ctx.update_message("No tiled panes found."),
Some(tid) => {
if ctx
.layout_tree
.split_pane(
tid,
id,
crate::editor::layout::SplitDirection::Vertical,
0.5,
)
.is_ok()
{
if let Some(pane) = ctx.pane_manager.get_pane_mut(id) {
pane.is_floating = false;
pane.is_minimized = false;//ignore it for now
}
let size = ctx.terminal_size;
ctx.handle_resize(size);// forces a redraw too
ctx.update_message(&format!("Pane {} is now tiled", id));
} else {
ctx.update_message("Failed to tile pane (target too small?)");
}
}
}
}
Ignore the ctx part for now, I have binded this with unfloat command from the command prompt. For now it just takes the id of the current active focused pane, and try to unfloat it, if it is already tiled we do not do anything and just show the message that the Pane is already tiled. Now, here you might ask why have I not integrated the flow for deleting the given a pane id. Well I am just keeping it simple and to add that feature is but just a small task. Now lets see how have implemented float for a tiled pane:
// see we just want to toggle one flag(is_floating) and call for a redraw again
// while also expanding the split node to contain the other leaf node only
// or we can just say that we have removed that pane from the layout tree
// though it still exists in the Pane manager
pub fn toggle_floating(id: usize, ctx: &mut EditorContext) {
let is_floating = ctx
.pane_manager
.get_pane(id)
.map_or(false, |p| p.is_floating);
if is_floating {
ctx.update_message("Pane is already floating.");
return;
}
let was_active = ctx
.pane_manager
.active_pane()
.map(|p| p.pane_id == id)
.unwrap_or(false);
if ctx.layout_tree.remove_node(id).is_err() {
ctx.update_message("Cannot float the last tiled pane!");
return;
}
if let Some(pane) = ctx.pane_manager.get_pane_mut(id) {
pane.is_floating = true;
let mut rect = pane.component().rect();
// I thought I should for now give them a default size too
rect.size.height = rect.size.height.min(15);
rect.size.width = rect.size.width.min(40);
rect.position.col = rect
.position
.col
.min(ctx.terminal_size.width.saturating_sub(4));
rect.position.row = rect
.position
.row
.min(ctx.terminal_size.height.saturating_sub(3));
pane.resize(rect);
}
// and rather than relying on Z indexes ( it was causing me some bugs)
// we just call to a fxns bring_to_front(pane_id);
ctx.pane_manager.bring_to_front(id);
let size = ctx.terminal_size;
ctx.handle_resize(size);
if was_active {
ctx.pane_manager.set_active_pane(id);// focus on this pane now
}
ctx.update_message(&format!("Pane {} is now floating", id));
}
Now let’s discuss this bring to the front. Like if I have multiple floating panes open and I focus on the first one I created or like in this diagram the 4 one, this my caret should go into the pane with id=4 and a border should be drawn to distinguish it too. As we know we are initializing the z idx with 0 now we need to handle it for a new floating pane. To get the correct following sequence.

Fig: caret focusing in the floating Pane 1

Fig: caret focusing in Pane 4 after we clicked on the Pane 4
Now, again we go by simple to complex approaches to handle this :
First Approach (Very Simple)
We find the max_z from all the floating panes and increase this every time we have a new pane.
pub fn bring_to_front(&mut self, pane_id: usize) {
if let Some(pane) = self.panes.get(&pane_id) {
if !pane.is_floating {
return;
}
} else {
return;
}
let max_z = self
.panes
.values()
.filter(|p| p.is_floating)
.map(|p| p.z_index)
.max()
.unwrap_or(0);
if let Some(pane) = self.panes.get_mut(&pane_id) {
pane.z_index = max_z + 1;
}
}
But there is a problem in this approach assume that we created two floating panes and now we are alternating between them, our z is always going up by 1. And this code smells because, see if we are always updating the value and creating a new z_index till usize. Like this approach works ofc, and we may never hit that limit but still why update everytime?
Keep in mind that a normal tiled pane will have a z_index = 0. A newly created floating pane will have a z_index=1.
Second Approach (Mine + Currently in use )
So, I thought of a better approach, what if we just swap the two z_indexes, like see if the z-indexes are equal only then do we update or bump the z_idx else we just swap like see->we already had one existing floating pane and we created one floating pane so max_z=1 (before making a new floating pane) and our z_index = 1 then we just bump our current z_index =1 + 1 =2 we create another floating pane max_z =2, and ours is 1 still so rather than updating it we just swap. now new Pane’s z_index = 2 and the other pane’s z_index which was focused before is equal to 1 now. See by using this approach we were able to create a max of three layers:
z_index=0 => Tiled Panes z_index=1 => floating panes (in some cases, active too) z_index=2 => currently active floating pane
This approach will achieve our required outcome. And I reached this approach following the approach of first principles. I felt pretty good when I reached this approach. Now even if we have a Tiled pane(z_index=0) wanting to become a floating focused guy, it can directly swap with the layer 1 or layer 2 directly.
pub fn bring_to_front(&mut self, pane_id: usize) {
if !self.panes.get(&pane_id).map_or(false, |p| p.is_floating) {
return;
}
let Some(top_id) = self
.panes
.values()
.filter(|p| p.is_floating)
.max_by_key(|p| p.z_index)
.map(|p| p.pane_id)
else {
return;
};
let top_z = self.panes[&top_id].z_index;
let target_z = self.panes[&pane_id].z_index;
if top_z == target_z {
// tie so we just bump
self.panes.get_mut(&pane_id).unwrap().z_index = top_z + 1;
} else {
// now we just swap
self.panes.get_mut(&top_id).unwrap().z_index = target_z;
self.panes.get_mut(&pane_id).unwrap().z_index = top_z;
}
}
Alternative approaches (I discussed with Claude)
Ordered list instead of z_index at all:
I could see this one working too like, instead of keeping track of z_indexes we put the render order in a Vec<usize>. Like hmm, Keep a floating_order: Vec<usize> on the PaneManager , just a list of pane ids, bottom to top. bring_to_front becomes: remove the id from wherever it sits, push it to the end. Render order is just "iterate the vec." No z_index field needed on Pane at all and the position in the list is the z-index. And this was a cool approach too, I could see this one working too, but again what if I had too many panes and this gave relatively same Time complexity to focus on a pane as my approach and took more space.
pub fn bring_to_front(&mut self, pane_id: usize) {
if !self.panes.get(&pane_id).map_or(false, |p| p.is_floating) {
return;
}
self.floating_order.retain(|&id| id != pane_id);
self.floating_order.push(pane_id);
}
It’s most appealing factor is that it preserves the realtive ordering among 3+ floating panes, where two z-values can’t represent a real stacking history once there are more than 2 panes. But I didn’t actually care about the relative ordering of these floating panes at all. I am just concerned with being the (the one on top) focusedone or not. And I am sure Manglu wouldn’t mind this tradeoff either as, we might have to limit the number of floating panes in future too or maybe implement minimization(hehe).
Natural incrementing z_index at creation:
This one will follow the the previous approach that we followed with the next_pane_id in the Pane Manager where we incremented this value for each new pane
pub struct PaneManager {
panes: HashMap<usize, Pane>,
active_pane: usize,
next_pane_id: usize,
next_z_index: usize,
}
Okay,so what this one will do is just kill the tie case at source, but again no panes would be born equal. To implement bring_to_front, I will just have to swap in this case. Again not much to gain from this approach.
Finalised Flow :
refresh_screen()
│
┌─────────────┴─────────────┐
│ │
▼ ▼
Render Layout Tree Render Floating
(Layer 0) (Sorted by z)
Pane1 Pane7
Pane2 Pane9
Pane5 Pane4
└─────────────┬─────────────┘
▼
Flush Terminal
So, Folks now we have a Good enough idea on how to implement and also an overall idea for the flow of rendering the floating panes. I will not go into the details about the drag functionality for this part. Because I have covered something similar in the last Part where I was building the Pane Abstraction. You can check that out here : https://medium.com/@pumkininriver/pane-abstraction-in-thetext-editor-rust-that-i-am-building-to-use-as-writer-f4296c77372f?sharedUserId=pumkininriver
Or you can directly check this out on my Github here : https://github.com/akshayrivers/Text_Editor
Okay now Manglu won’t swing his sword at Yonro due to his frustration. Yonro can breathe better now.
Manglu: “WHY THE HELL IS SAME FILE OPENING EVERYTIME I CREATE A SPLIT OR A FLOATING PANE. YONRO YOU HEATHEN!!! YOU WANT TO DIE??? FIX IT OR I WILL CALL UPON THE WRATH OF AN ANGEL(THE OS) TO STRIKE YOU DOWN (KILL THE PROCESS). FIX THIS NUISANCE AT ONCE!!!”
Crap! and here I thought that Yonro was safe. Huh that thing is because we are dyanmically dispatching the same view to the Pane. And under the hood every view is sharing only one buffer, we need to fix that. So that each new Text view gets it’s own buffer. And if two panes share share the same buffer ID changes in one should propogate into the other. Phew, need to build all that perfectly or that mad knight might just swing his sword at the screen.
“Okay let us go again by first principles again. As we can have — “ I don’t even get to finish. Manglu: “HEATHEN! IF THIS PROBLEM DOESN’T GO IT SELF I WILL PLUNDER YOUR WHOLE VILLAGE!”
Uhh, change of plans, we fix the issues on a rolling basis and then optimise once the mad man can write correctly.
Buffer Manager :
View currently owns a Buffer directly:
View { buffer: Buffer } ← ownership, not a shared reference
But splits/floats were dispatching the same View instance to multiple panes:
Pane A ──┐
├──► same View instance ──► same Buffer
Pane B ──┘ (dispatched twice, not two separate Views)
Text View Struct looks something like this :
#[derive(Default)]
pub struct View {
id: usize,
is_active: bool,
buffer: Buffer,// see this is the part we need to resolve
// right now the view is owning a buffer but it should own a buffer_id only
needs_redraw: bool,
rect: Rect,
text_location: Location,
scroll_offset: Position,
search_info: Option<SearchInfo>,
undo_stack: Vec<EditOperation>,
redo_stack: Vec<EditOperation>,
last_insert_time: Option<Instant>,
last_insert_location: Option<Location>,
}
Lets look at what Buffer looks like :
#[derive(Default)]
pub struct Buffer {
lines: Vec<Line>,
file_info: FileInfo,
dirty: bool,
}
The buffer looks pretty simple, The only complexity is in it’s implementation. But what if we just create a unique id for each buffer and share it with the Text View, (can you see that in future with plugins we will just have to share the buffer_id with the plugins too. like if there is a huge file, we render only the visible part and render in real time rather than keeping all the file in memory. And a plugin which needs the context of the the whole “HAAAAAAHHHHHHH !!! Heathennn!!”
Crap! So we just need a hashmap for that which we will call our buffer manager, something like this:
use crate::editor::buffers::Buffer;
use std::collections::HashMap;
#[derive(Default)]
pub struct BufferManager {
buffers: HashMap<usize, Buffer>,
next_buffer_id: usize,
}
impl BufferManager {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, buffer: Buffer) -> usize {
let id = self.next_buffer_id;
self.buffers.insert(id, buffer);
self.next_buffer_id += 1;
id
}
pub fn get(&self, id: usize) -> Option<&Buffer> {
self.buffers.get(&id)
}
pub fn get_mut(&mut self, id: usize) -> Option<&mut Buffer> {
self.buffers.get_mut(&id)
}
pub fn remove(&mut self, id: usize) -> Option<Buffer> {
self.buffers.remove(&id)
}
pub fn iter(&self) -> impl Iterator<Item = (&usize, &Buffer)> {
self.buffers.iter()
}
}
phew and in the View we just do :
#[derive(Default)]
pub struct View {
id: usize,
buffer_id: usize,// Just a buffer ID
is_active: bool,
needs_redraw: bool,
rect: Rect,
text_location: Location,
scroll_offset: Position,
search_info: Option<SearchInfo>,
undo_stack: Vec<EditOperation>,
redo_stack: Vec<EditOperation>,
last_insert_time: Option<Instant>,
last_insert_location: Option<Location>,
}
// inside the impl block :
pub fn set_buffer_id(&mut self, id: usize) {
self.buffer_id = id;
self.text_location = Location::default();
self.scroll_offset = Position::default();
self.mark_redraw(true);
}
Now from the main editor Loop on split or floating Pane we will dispatch a newly created buffer and then bind it to a text view through a buffer_id. And then this Text View will be Dispatched to the Pane Content dynamically.
//As I said do not pay any heed to the ctx I will explain it later
fn split_active_pane(ctx: &mut EditorContext, direction: SplitDirection) {
let active_id = match ctx.pane_manager.active_pane().map(|p| p.pane_id) {
Some(id) => id,
None => return,
};
// creating a new id for every new buffer
let new_buffer_id = {
let buffer = Buffer::default();
ctx.buffer_manager.add(buffer)
};
// a new pane ID
let new_pane_id = {
let mut view = View::default();//a new View
view.set_buffer_id(new_buffer_id);// Binding the new_buffer_id
// now we dynamically dispatch the Text View
ctx.pane_manager.create_pane(PaneContent::TextView(view))
};
if let Some(pane) = ctx.pane_manager.get_pane_mut(new_pane_id) {
if let Some(view) = pane.view_mut() {
view.set_id(new_pane_id);
}
}
if ctx
.layout_tree
.split_pane(active_id, new_pane_id, direction, 0.5)
.is_err()
{
ctx.update_message("Pane too small to split");
ctx.pane_manager.remove_pane(new_pane_id);
return;
}
let size = ctx.terminal_size;
ctx.handle_resize(size);
ctx.pane_manager.set_active_pane(new_pane_id);
}
Final Flow :
View1 ──┐
├──► buffer_id ──► BufferManager ──► Buffer A
View2 ──┘ (each view has it's own id)
BufferManager ──► Buffer B
View1 (buffer_id: 0) ──► BufferManager[0] ──► Buffer A
View2 (buffer_id: 1) ──► BufferManager[1] ──► Buffer B
Manglu: “Oh! It seems to be working quite good right now. Do not worry Yonro , I was just jesting . Please forgive me for my transgression. I also apologise for the threats to your life. I have just come back from a Battle. Do not pay my words any regards moving forward.”
Yeah, maybe this part you had to say in the beginning. It would have been so nice. Well works for me as there is still one point where he can frustrated. Yonro currently has no way of opening the same file in multiple panes. Well let’s just hope the brute won’t have a need for it anytime soon. It will be handled and solved in the next blog. Hope to see you there again
— — — — — — — — — — — — — The Tale Ends here — — — — — — — — — — — — — — —
Thank you for reading this Blog, I hope you enjoyed it.
— — — — — — — — — — — — — — — — Yapping start — — — — — — — — — — — — — — —
I was a bit frustrated about not being able to work on my stories due to ,,,, due to preparing for interviews, doing DSA and strengthening my Core subjects, And on the side work on my Projects. All to find an internship. Yeah that is a different tale filled with anguish. The time I am writing this blog, I have applied at lots of places, have been ghosted by many, have heard back from a few ( the letters starting with unfortunately ). Still waiting on some news, the results days of few are still far. I hope to get an internship or some experience from some experienced Developers. Sorry for the little bit of yapping.
— — — — — — — — — — — — — — — —Yapping End — — — — — — — — — — — — — — —
As of writing this Blog, I have already implemented the Plugin architecture and have also create the first Plugin- a File explorer. Now that had been one of the hardest, if not the hardest thing that I have done, since I have started my Engineering degree. The implementation part is not that hard if you know the language well. The hardest part was designing it. Designing the Plugin Architecture. I wasn’t able to coast through with just using the First Principles. I discussed it with so many AI’s — the best approach, the simple approach, How others have implemented it? And this was one of the places where I reached the conclusion of using the Asynchronous Programming for Plugins. It was very interesting to move out of theory from OSTEP and the Rust Book to implement a complex asynchronous flow in my code. And again the problem wasn’t of coding, it was of design. I had hit a wall here.
Again, Thank you for reading this. Please leave your thoughts in the comments or you can Mail me at : akshayforrivers@gmail.com Dm me on X : https://x.com/Vinodakshat1 Dm me on Instagram: https://www.instagram.com/vinodakshat/ My Github : https://github.com/akshayrivers Linkedin: https://www.linkedin.com/in/vinod-akshat?originalSubdomain=in
메타데이터
- post_id
- ba4d12b0c2a5
- slug
- implementing-floating-panes-and-a-buffer-manager-in-my-text-editor-yonro-in-form-of-a-whimsy-ba4d12b0c2a5
- url
- https://medium.com/@pumkininriver/implementing-floating-panes-and-a-buffer-manager-in-my-text-editor-yonro-in-form-of-a-whimsy-ba4d12b0c2a5
- canonical_url
- https://medium.com/@pumkininriver/implementing-floating-panes-and-a-buffer-manager-in-my-text-editor-yonro-in-form-of-a-whimsy-ba4d12b0c2a5
- author_url
- https://medium.com/@pumkininriver
- status
- ok
- fetched_at
- 2026-07-10 21:04:09