Writing a Simple Text Editor

nakst  —  3 weeks ago [Edited 1 week, 5 days later]
I recently decided to make a version of "Notepad" for my OS, and it's mostly done now.
To take a deeper look into using the OS's API, I'll walk you through how you could recreate the program.
(Note: the API is still under constant revision, so any information presented in this blog could change at any time...)

We can start by creating a manifest file. The build system should automatically find it.
Let's add a definition for the program.

text_editor/text_editor.manifest
1
2
3
4
[program]
name = "Text Editor";
shortName = "text_editor";
systemMessageCallback = ProcessSystemMessage;


This specifies the name of the program, its internal name, and the subroutine that will receive system messages. We'll take a look at this later.

Next we include information about how to build the program.

1
2
3
4
5
[build]
output = "Text Editor.esx";
source = "text_editor/main.cpp";
installationFolder = "/Programs/Text Editor/";
Install;


This gives the source file, the output executable, and the installation folder.
It also tells the build system to automatically install it. This just means a few lines will get added to "bin/OS/Installed Programs.dat".
Program installation is nothing fancy in the OS.

Now let's define the menus for the program.
We will be using the default file and edit menus, so we don't need to define those. Just a search menu, and the menubar.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[menu menuSearch]
name = "Search";
commandFind;
commandReplace;
Separator;
commandFindNext;
commandFindPrevious;

[menu mainMenubar]
osMenuFile;
osMenuEdit;
menuSearch;


This should be fairly self explanatory.

Now let's define the commands in the search menu.
I'll explain exactly what a "command" is in a bit.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
[command commandFind]
label = "Find...";
shortcut = "Ctrl+F";

[command commandReplace]
label = "Replace...";
shortcut = "Ctrl+H";

[command commandFindNext]
label = "Find next";
shortcut = "F3";
defaultDisabled = true;

[command commandFindPrevious]
label = "Find previous";
shortcut = "Shift+F3";
defaultDisabled = true;


This is a fairly convenient way to define the labels and keyboard shortcuts for different commands.
All the commands we define in the manifest are also automatically added to the default command group, in which they are assigned an index starting from 0.
We disable the find next/previous commands by default, as we only want to enable these once the user has put some text in the find/replace textboxes in the dialogs we'll be making.

There are just a few more commands we'll need.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[command commandReplaceNext]
label = "Replace next";
defaultDisabled = true;

[command commandReplaceAll]
label = "Replace all";
defaultDisabled = true;
dangerous = true;

[command commandCloseDialog]
label = "Close";

[command commandMatchCase]
label = "Match case";
checkable = true;


These will be needed for the find/replace dialogs.
We mark the "replace all" command as "dangerous". This means buttons bound to that command will glow red when the user hovers over them.
It's purely a visual difference - but it should help the user to understand that the single button will cause a lot of modifications to their document.
The "match case" command is marked as checkable - this is because we'll be using it to create a checkbox. You could also put this command in the search menu, where it would become a checkable menu item.

At the end of our manifest file, we can include the templates for our windows and dialogs.
Currently there is no way to specify the contents of windows in the manifest file, so we'll have to do this in the code.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
[window mainWindow]
width = 330;
height = 300;
minimumWidth = 250;
minimumHeight = 250;
title = "Text Editor";
menubar = mainMenubar;

[window dialogFind]
width = 360;
height = 100;
resizable = false;
title = "Find";

[window dialogReplace]
width = 360;
height = 130;
resizable = false;
title = "Replace";


As you can see, we can however define the dimensions and titles of the windows/dialogs.
One convenience of the manifest files is that we can exclude any properties we don't care about, and they will be automatically set to sane defaults.
This is why we don't have to mark the mainWindow as resizable - this is the default.

Okay, on to the C++!

text_editor/main.cpp
1
2
3
4
#include "../api/os.h"

#define OS_MANIFEST_DEFINITIONS
#include "../bin/Programs/Text Editor/manifest.h"


We start by including the API's header file, and the automatically-generated definitions from our manifest.

Now, since the program will handle multiple documents from a single process, we need to make a structure to store all the information about one "Instance" of the program.

1
2
3
4
5
6
7
struct Instance {
	OSObject window, textbox, 
		 findDialog, replaceDialog,
		 findTextbox, replaceTextbox;
	OSString findString, replaceString;
	OSCommand *commands;
};


The API provides opaque handles to its objects, such as windows and controls through the type "OSObject".
The instance will store a handle to the window, the main textbox, the find/replace dialogs, and the textboxes within the find/replace dialogs.
We also have two strings (a buffer + byte count), for the current find query, and the replacement text.
Finally, we store an array of commands. When each instance is created we will create this array, and in it the state of each command can be stored. This includes whether the command is disabled, checked, the notification callback, etc.

It's also important to note at this point that the API also has a notion of instances.
It will have its own structure for each instance of the program that is started, where it keeps its own information. This will include, for example, the builtin commands (copy, paste, save file, etc.). Hopefully it's clear whether the API's instance object or the program's instance structure is being referred to when they're used. Now, onto the first of the two subroutines that make up this program.
1
2
3
4
OSCallbackResponse ProcessSystemMessage(OSObject, OSMessage *message) {
	...
	return OS_CALLBACK_NOT_HANDLED;
}


This is the subroutine in which the API will send system messages to us.
The name of this subroutine is determined in the program section of the manifest, as shown above.

We only need to handle one system message.

1
2
3
4
if (message->type == OS_MESSAGE_CREATE_INSTANCE) {
	...
	return OS_CALLBACK_HANDLED;
}


This is sent when we need to start a new instance of our program.
For example, the program executable might be double-clicked in the file manager, or a text file gets opened (not implemented yet).

Before we can create the window, we should start an allocation block.

1
2
3
OSStartGUIAllocationBlock(16384);
...
OSEndGUIAllocationBlock();


This will allocate a 16KB chunk of memory. This will then be split up as we create the window and its contents.
When the window is closed, this memory chunk will get deallocated.

Okay, let's allocate an Instance.

1
Instance *instance = (Instance *) OSHeapAllocate(sizeof(Instance), true /*clear to zero*/);


...and all the commands in the default command group (those are the ones in our manifest).

1
2
instance->commands = OSCreateCommands(osDefaultCommandGroup);
OSSetCommandGroupNotificationCallback(instance->commands, ProcessNotification);


We also set their notification callback.
A notification callback is the subroutine to which "notifications" are sent. Notifications range from things like invoking a command, to a list view wanting a row repainted.
The "context" parameter of the notification is automatically assigned to the index of the command in the command array.
We'll see how this works in a moment.

Here's where we create the API instance - "instanceObject".
1
OSObject instanceObject = OSCreateInstance(instance, message, instance->commands);


We pass in a pointer to our Instance structure - "instance". This is so whenever we receive a notification we will get a pointer to our Instance structure in the "instanceContext" field.
We also pass in the create instance message structure we received. This contains flags and other information that the creating process set.
Finally we pass in a pointer to our command array. This is so the API is aware of our commands - including useful things like making keyboard shortcuts work.

Great! Now we can make our window.

1
instance->window = OSCreateWindow(mainWindow, instanceObject);


We pass in the window template from our manifest, and the API instance object.

In the window we need a textbox, so let's make that.

1
instance->textbox = OSCreateTextbox((OSTextboxStyle) (OS_TEXTBOX_STYLE_MULTILINE | OS_TEXTBOX_STYLE_NO_BORDER), OS_TEXTBOX_WRAP_MODE_NONE);


We add it to a 1x1 grid so that it fills it ("OS_CELL_FILL").

1
2
OSObject grid = OSCreateGrid(1 /*columns*/, 1 /*rows*/, OS_GRID_STYLE_LAYOUT /*no border or padding between cells*/);
OSAddControl(grid, 0 /*column*/, 0 /*row*/, instance->textbox, OS_CELL_FILL);


We then set the grid as the root of the window, and then make the textbox the default focused control.

1
2
OSSetRootGrid(window, grid);
OSSetFocusedControl(textbox, true /*default*/);


By setting it as the default focused control, if the user presses the "escape" key in the window, the keyboard focus will go to the textbox.

Now, our program uses a single subroutine - ProcessNotification - to handle all of the notifications.
As noted earlier, we set the callback context for our commands to be their index into the command group.
We want to receive notifications from other sources, such as the textbox, so we need to come up with values to use, so we can identify them as the notification source in the callback.
Let's use negative values so they don't conflict with the commands.
1
2
3
4
5
#define WINDOW_NOTIFICATION (-1)
#define TEXTBOX_NOTIFICATION (-2)
#define INSTANCE_NOTIFICATION (-3)
#define FIND_TEXTBOX_NOTIFICATION (-4)
#define REPLACE_TEXTBOX_NOTIFICATION (-5)


Then we can actually set the notification callbacks...

1
2
3
OSSetObjectNotificationCallback(window, OS_MAKE_NOTIFICATION_CALLBACK(ProcessNotification, (intptr_t) WINDOW_NOTIFICATION));
OSSetObjectNotificationCallback(textbox, OS_MAKE_NOTIFICATION_CALLBACK(ProcessNotification, (intptr_t) TEXTBOX_NOTIFICATION));
OSSetObjectNotificationCallback(instanceObject, OS_MAKE_NOTIFICATION_CALLBACK(ProcessNotification, (intptr_t) INSTANCE_NOTIFICATION));


We'll take a look at what notifications we get from these sources in a bit.

Finally we need to set the window title.

1
OSSetWindowTitle(window, OSLiteral("Untitled"));


This will be prefixed to the title we defined in the manifest.

Okay! We're making some good progress.
We now need to make the notification callback itself.

1
2
3
4
OSCallbackResponse ProcessNotification(OSNotification *notification) {
	...
	return OS_CALLBACK_NOT_HANDLED;
}


For our convenience, we first extract some information from the "notification" structure.

1
2
bool isCommand = notification->type == OS_NOTIFICATION_COMMAND;
Instance *instance = (Instance *) notification->instanceContext;


"isCommand" will indicate whether this notification was sent because of a command invocation (e.g. pressing a button, menu item or keyboard shortcut).
"instance" will contain a pointer to our instance structure. "notification->instance" will contain the API instance object.

Now we can switch on the notification source:

1
2
switch ((intptr_t) notification->context) {
}


The first notification we need to handle is when the contents of the textbox is modified.
We will need to mark the instance as modified, so that the "Save" command will be enabled, and attempting to use the "Open" command will prompt the user to save.

1
2
3
4
5
6
case TEXTBOX_NOTIFICATION: {
	if (notification->type == OS_NOTIFICATION_MODIFIED) {
		OSMarkInstanceModified(notification->instance);
		return OS_CALLBACK_HANDLED;
	}
} break;


We don't actually have to do any save/open confirmation dialog nonsense, since it's all handled by the API through the builtin commands.
It will just send us notifications telling exactly what to do.
Speaking of which...

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
case INSTANCE_NOTIFICATION: {
	if (notification->type == OS_NOTIFICATION_NEW_FILE) {
		// Clear the text in the textbox.
		OSSetText(instance->textbox, OSLiteral(""), OS_RESIZE_MODE_IGNORE);

		// Move the caret to the start of the textbox.
		OSTextboxSetSelection(instance->textbox, 0, 0);
	} else if (notification->type == OS_NOTIFICATION_OPEN_FILE) {
		// Load the file.
		size_t fileSize;
		char *text = (char *) OSReadEntireFile(notification->fileDialog.path, notification->fileDialog.pathBytes, &fileSize); 

		if (text) {
			// Set the textbox's text.
			OSSetText(instance->textbox, text, fileSize, OS_RESIZE_MODE_IGNORE);

			// Move the caret to the start of the textbox.
			OSTextboxSetSelection(instance->textbox, 0, 0);

			// And deallocate the text.
			OSHeapFree(text);
		} else {
			// If we couldn't read the file, cause a error dialog to appear.
			return OS_CALLBACK_REJECTED;
		}
	} else if (notification->type == OS_NOTIFICATION_SAVE_FILE) {
		// Get the text in the textbox.
		OSString text;
		OSGetText(instance->textbox, &text);

		// Open the file we'll be saving to.
		OSNodeInformation node;
		OSError error = OSOpenNode((char *) notification->fileDialog.path, notification->fileDialog.pathBytes, 
				OS_OPEN_NODE_WRITE_EXCLUSIVE | OS_OPEN_NODE_RESIZE_EXCLUSIVE | OS_OPEN_NODE_CREATE_DIRECTORIES, &node);

		if (error == OS_SUCCESS) {
			// Write the contents of the textbox and close the file handle.
			error = text.bytes == OSWriteFileSync(node.handle, 0, text.bytes, text.buffer) ? OS_SUCCESS : OS_ERROR_UNKNOWN_OPERATION_FAILURE; 
			OSCloseHandle(node.handle);
		}

		if (error != OS_SUCCESS) {
			// If there was an error, cause a error dialog to appear.
			notification->fileDialog.error = error;
			return OS_CALLBACK_REJECTED;
		}
	} else {
		return OS_CALLBACK_NOT_HANDLED;
	}

	return OS_CALLBACK_HANDLED;
} break;


Next we need to handle the OS_NOTIFICATION_WINDOW_CLOSE notification from the window.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
case WINDOW_NOTIFICATION: {
	if (notification->type == OS_NOTIFICATION_WINDOW_CLOSE) {
		OSDestroyCommands(instance->commands);
		OSDestroyInstance(notification->instance);
		OSHeapFree(instance->findString.buffer);
		OSHeapFree(instance->replaceString.buffer);
		OSHeapFree(instance);
		return OS_CALLBACK_HANDLED;
	}
} break;


We destroy the command array, and the API instance object.
And then we free our Instance structure and its contents.
The finer details of window/instance closing isn't really finished yet, but it works for the most part at the moment.

Right. Now let's handle the commandFind and commandReplace commands. This is where we're create the find/replace dialogs.
Since commandFind is just a simpler version of commandReplace, we'll just focus on commandReplace for now.
Here's a reference of how we will layout the dialog.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
case commandReplace: {
	// Is this a command notification?
	if (isCommand) { 
		// If the replace dialog was already open, set it as the focused window.
		if (instance->replaceDialog) {
			OSSetFocusedWindow(instance->replaceDialog);
			return OS_CALLBACK_HANDLED;
		}

		// If the find dialog was open, close it.
		if (instance->findDialog) {
			OSCloseWindow(instance->findDialog);
			instance->findDialog = nullptr;
		}

		OSStartGUIAllocationBlock(16384);

		// Create a dialog using the dialogReplace template.
		instance->replaceDialog = OSCreateDialog(notification->instance, nullptr /*don't block input to the main window*/, dialogReplace);

		// Create a 1x2 grid at the root of the dialog.
		OSObject root = OSCreateGrid(1 /*columns*/, 2 /*rows*/, OS_GRID_STYLE_LAYOUT);
		OSSetRootGrid(instance->replaceDialog, root);

		// At the top of the grid, put the options. This is another 1x2 grid.
		OSObject options = OSCreateGrid(1, 2, OS_GRID_STYLE_CONTAINER /*include a border and padding between cells*/);
		OSAddGrid(root, 0 /*column*/, 0 /*row*/, options, OS_CELL_FILL); // Fill the dialog as much as possible.

		// Add the match case checkbox to the options grid.
		OSAddControl(options, 0, 1, OSCreateButton(instance->commands + commandMatchCase, OS_BUTTON_STYLE_NORMAL), OS_CELL_H_LEFT);

		// Create the textboxes grid, 2x2, and add it to the options grid.
		OSObject textboxes = OSCreateGrid(2, 2, OS_GRID_STYLE_CONTAINER_WITHOUT_BORDER);
		OSAddGrid(options, 0, 0, textboxes, OS_CELL_H_FILL);

		// Create the find textbox, set its notification callback, and add it with its label to the grid.
		instance->findTextbox = OSCreateTextbox(OS_TEXTBOX_STYLE_NORMAL, OS_TEXTBOX_WRAP_MODE_NONE);
		OSSetObjectNotificationCallback(instance->findTextbox, OS_MAKE_NOTIFICATION_CALLBACK(ProcessNotification, (intptr_t) FIND_TEXTBOX_NOTIFICATION));
		OSAddControl(textboxes, 0, 0, OSCreateLabel(OSLiteral("Find what:"), false, false), OS_CELL_H_RIGHT /*Align the label to the right*/);
		OSAddControl(textboxes, 1, 0, instance->findTextbox, OS_CELL_H_FILL /*Fill the dialog horizontally with the textbox*/);

		// Repeat for the replace textbox.
		instance->replaceTextbox = OSCreateTextbox(OS_TEXTBOX_STYLE_NORMAL, OS_TEXTBOX_WRAP_MODE_NONE);
		OSSetObjectNotificationCallback(instance->replaceTextbox, OS_MAKE_NOTIFICATION_CALLBACK(ProcessNotification, (intptr_t) REPLACE_TEXTBOX_NOTIFICATION));
		OSAddControl(textboxes, 0, 1, OSCreateLabel(OSLiteral("Replace with:"), false, false), OS_CELL_H_RIGHT);
		OSAddControl(textboxes, 1, 1, instance->replaceTextbox, OS_CELL_H_FILL);

		// Create the commands grid, 4x1.
		OSObject commands = OSCreateGrid(4, 1, OS_GRID_STYLE_CONTAINER_ALT /*use the alternate style*/);
		OSAddGrid(root, 0, 1, commands, OS_CELL_H_FILL);

		// Add a spacer at the start so that the buttons are all aligned to the right.
		OSAddControl(commands, 0, 0, OSCreateSpacer(0, 0), OS_CELL_H_FILL);

		// Create the buttons and add them to the grid.
		// By specifying the command for the button, they will automatically be labelled and disabled/enabled with the command.
		OSObject defaultButton = OSCreateButton(instance->commands + commandReplaceNext, OS_BUTTON_STYLE_NORMAL);
		OSAddControl(commands, 1, 0, defaultButton, OS_FLAGS_DEFAULT);
		OSAddControl(commands, 2, 0, OSCreateButton(instance->commands + commandReplaceAll, OS_BUTTON_STYLE_NORMAL), OS_FLAGS_DEFAULT);
		OSAddControl(commands, 3, 0, OSCreateButton(instance->commands + commandCloseDialog, OS_BUTTON_STYLE_NORMAL), OS_FLAGS_DEFAULT);

		// Set the default button and the focused control (the find textbox).
		OSSetFocusedControl(defaultButton, false);
		OSSetFocusedControl(instance->findTextbox, false);

		// If the user presses the "escape" key in the dialog, route it to do the same thing as commandCloseDialog.
		OSSetCommandNotificationCallback(OSGetDialogCommands(instance->replaceDialog) + osDialogStandardCancel, 
				OS_MAKE_NOTIFICATION_CALLBACK(ProcessNotification, (intptr_t) commandCloseDialog));

		OSEndGUIAllocationBlock();

		return OS_CALLBACK_HANDLED;
	}
} break;


Let's handle the commandCloseDialog command.
This just closes the open dialog when invoked.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
case commandCloseDialog: {
	if (isCommand) {
		if (instance->replaceDialog) {
			OSCloseWindow(instance->replaceDialog);
			instance->replaceDialog = nullptr;
		}

		if (instance->findDialog) {
			OSCloseWindow(instance->findDialog);
			instance->findDialog = nullptr;
		}

		return OS_CALLBACK_HANDLED;
	}
} break;


When the find/replace textboxes are modified, we need to copy their contents into our strings, and also enable/disable the relevant commands.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
case FIND_TEXTBOX_NOTIFICATION: {
	if (notification->type == OS_NOTIFICATION_MODIFIED) {
		OSString text;
		OSGetText(instance->findTextbox, &text);
		OSHeapFree(instance->findString.buffer);
		instance->findString.buffer = (char *) OSHeapAllocate(text.bytes, false);
		OSCopyMemory(instance->findString.buffer, text.buffer, text.bytes);
		instance->findString.bytes = text.bytes;

		OSEnableCommand(instance->commands + commandFindNext, instance->findString.bytes);
		OSEnableCommand(instance->commands + commandFindPrevious, instance->findString.bytes);
		OSEnableCommand(instance->commands + commandReplaceNext, instance->findString.bytes && instance->replaceString.bytes);
		OSEnableCommand(instance->commands + commandReplaceAll, instance->findString.bytes && instance->replaceString.bytes);
	}
} break;

case REPLACE_TEXTBOX_NOTIFICATION: {
	if (notification->type == OS_NOTIFICATION_MODIFIED) {
		OSString text;
		OSGetText(instance->replaceTextbox, &text);
		OSHeapFree(instance->replaceString.buffer);
		instance->replaceString.buffer = (char *) OSHeapAllocate(text.bytes, false);
		OSCopyMemory(instance->replaceString.buffer, text.buffer, text.bytes);
		instance->replaceString.bytes = text.bytes;

		OSEnableCommand(instance->commands + commandReplaceNext, instance->findString.bytes && instance->replaceString.bytes);
		OSEnableCommand(instance->commands + commandReplaceAll, instance->findString.bytes && instance->replaceString.bytes);
	}
} break;


Finally, when we get a find/replace command, we need to actually do it......
I won't explain this as it isn't particularly relevant to this blog post. But rest assured it does finding and replacing :)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
case commandReplaceAll:
case commandReplaceNext:
case commandFindPrevious:
case commandFindNext: {
	if (isCommand) {
		OSString text;
		OSGetText(instance->textbox, &text);

		bool firstPass = true;
		bool matchCase = OSGetCommandCheck(instance->commands + commandMatchCase);
		bool foundMatch = false;
		bool backwards = (intptr_t) notification->context == commandFindPrevious;
		bool replace = (intptr_t) notification->context == commandReplaceNext || (intptr_t) notification->context == commandReplaceAll;
		bool stopAfterMatch = (intptr_t) notification->context != commandReplaceAll;

		OSString query = instance->findString;
		OSString replacement = instance->replaceString;

		uintptr_t byte, byte2, start;
		OSTextboxGetSelection(instance->textbox, &byte, &byte2); 
		if (!backwards && byte2 > byte) byte = byte2;
		else if (backwards && byte > byte2) byte = byte2;
		if (backwards) byte--;
		start = byte;
		
		while (true) {
			if (!backwards) {
				if (firstPass && byte + query.bytes > text.bytes) {
					firstPass = false;
					byte = 0;
				} 

				if (!firstPass && (byte > start || byte + query.bytes > text.bytes)) {
					break;
				}
			} else {
				if (firstPass && byte > text.bytes) {
					firstPass = false;
					byte = text.bytes - query.bytes;
				} 

				if (!firstPass && (byte <= start || byte > text.bytes)) {
					break;
				}
			}

			bool fail = false;

			for (uintptr_t j = 0; j < query.bytes; j++) {
				uint8_t a = query.buffer[j];
				uint8_t b = text.buffer[j + byte];

				if (!matchCase) {
					if (a >= 'a' && a <= 'z') a += 'A' - 'a';
					if (b >= 'a' && b <= 'z') b += 'A' - 'a';
				} 

				if (a != b) {
					fail = true;
					break;
				}
			}

			if (!fail) {
				foundMatch = true;
				OSTextboxSetSelection(instance->textbox, byte, byte + query.bytes); 

				if (replace) {
					OSTextboxRemove(instance->textbox);
					OSTextboxInsert(instance->textbox, replacement.buffer, replacement.bytes);
				}

				if (stopAfterMatch) {
					break;
				}
			}

			if (!backwards) byte++; else byte--;
		}

		if (!foundMatch && stopAfterMatch) {
			OSShowDialogAlert(OSLiteral("Find"),
					OSLiteral("The find query could not be found in the current document."),
					OSLiteral("Make sure that the query is correctly spelt."),
					notification->instance,
					OS_ICON_WARNING, instance->window);
		}

		return OS_CALLBACK_HANDLED;
	}
} break;


And that's it!
You've written a simple text editor for my OS!



The API is still in development, so it's a bit rough around the edges, but I'm very happy with how it seems to be going.
It may seem a bit complicated, but once you understand it writing GUI programs becomes fairly easy, I've found.

If you have a question, please post a comment.

Thanks for reading! :)
#15880
Simon Anciaux  —  2 weeks, 6 days ago
The more things you add to the manifest files, the more I think it's a bad idea (But you already address that in a previous comment).

Could you post the complete code and manifest so we can have a better view of the wall program ?

If I understood correctly, there are two callbacks, one for system message and one for the commands/ui.
Have you thought about having no callback, but instead a user defined message loop. Similar to using GetMessage/PeekMessage on Windows, except every (as in there is no special case that wouldn't follow the rule) message would go through that. So we could choose when and how to handle every message.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
int main( void ) {
    
    while ( running ){
        
        // Messages
        while ( PeekMessage( message ) ){
             switch( message ){
                 ....
             }
        }

        // Update
        // Render
    }
}

I'm no sure I understand correctly why an "API instance" is needed. I think API is the wrong term here. It seems that it's just "application state". Couldn't we just pass the parameter you give to OSCreateInstance to OSCreateWindow to achieve the same think ?

Is it safe to do nothing in the close window notification, return from main and assume the os will release the handles and memory ?
#15883
nakst  —  2 weeks, 6 days ago
@mrmixer

The more things you add to the manifest files, the more I think it's a bad idea
What don't you like about them? They're mostly just used for automatic structure definitions, and a few build-related things.

Could you post the complete code and manifest so we can have a better view of the wall program ?
Bitbucket link, see the "text_editor" folder.

If I understood correctly, there are two callbacks, one for system message and one for the commands/ui.
Have you thought about having no callback, but instead a user defined message loop. Similar to using GetMessage/PeekMessage on Windows, except every (as in there is no special case that wouldn't follow the rule) message would go through that. So we could choose when and how to handle every message.
I have a slightly different way of doing game loops. After calling "OSSendIdleMessages(true)", whenever the message queue is empty, an idle message will be sent. The program can then do whatever it wants, and then can return to let the API to check and process the message queue again. I decided to do it like this so that the message loop is completely standardised between programs.

I'm no sure I understand correctly why an "API instance" is needed. I think API is the wrong term here. It seems that it's just "application state". Couldn't we just pass the parameter you give to OSCreateInstance to OSCreateWindow to achieve the same think ?
Hopefully this system will make more sense when I introduce tab based programs.

Is it safe to do nothing in the close window notification, return from main and assume the os will release the handles and memory ?
When a process exits the OS will release all its resources. However, because this one process will handle multiple windows and instances, we have to make sure that we clean up manually when a window is closed.

*

I'm fully aware that a lot of the features/ideas in the API are quite different to the hands-off approach of other operating systems. However, having written several programs for the OS so far, I'm finding it much easier to do everything than it other GUI frameworks I've used. Some features may have to be scrapped or changed, but I really like how it's working out. YMMV, I guess.
#15884
Simon Anciaux  —  2 weeks, 5 days ago
The [program] and [build] part of the manifest I don't mind. It looks like things for the build system/compiler so I'm ok with that.

The rest looks to me like an unnecessary level of indirection between me and the code. Most of it could be done in code with approximately the same amount of lines, and you could get/modify the values at runtime.
1
2
3
Type thing = GetDefault( );
thing.width = screenWidth / 2;
thing.param = value;
nakst
I have a slightly different way of doing game loops. After calling "OSSendIdleMessages(true)", whenever the message queue is empty, an idle message will be sent. The program can then do whatever it wants, and then can return to let the API to check and process the message queue again. I decided to do it like this so that the message loop is completely standardised between programs.
This was not a game loop, I use that same loop for application, but use GetMessage to wait for the first message and then PeekMessage to process the queue so that I process every pending messages before going to update and render.
The full code for the text editor doesn't contain a main function or any rendering call. How is that done ? Do you render after every processNotification ? Can you process every message before rendering ?

Also my question/suggestion was that I would like to have a unified way of processing messages (system and user messages in the same place) without using a callback so that I control when and how it's happening.

Could you show (in pseudo code) a game loop for your os ?
#15887
nakst  —  2 weeks, 5 days ago
The rest looks to me like an unnecessary level of indirection between me and the code. Most of it could be done in code with approximately the same amount of lines, and you could get/modify the values at runtime.

It is possible to make the definitions in code like you show.
However, I don't know how well it will play with configuration files, once I implement them. I ideally want the user to have full control over the contents of everything, without needing to recompile your program. This is why I decided to introduce the layer of indirection.
Furthermore, I find it quicker to update the manifest file. For example, forward references work just fine in the manifests unlike in C! It's just metaprogramming, which I think Is better than some crazy x-macros setup in C.


The full code for the text editor doesn't contain a main function or any rendering call. How is that done ? Do you render after every processNotification ? Can you process every message before rendering ?

There is no main function, because the text editor does not use the c runtime library. The API contains the program's entry point and will call into the ProcessSystemMessage subroutine in your program as necessary. If any global initialisation must be done, then it can be done when the first system message is received.

To understand when rendering happens, I need to explain how the messaging system works in greater detail.
Let's say the user clicks a button. The window manager will send a OS_MESSAGE_MOUSE_LEFT_RELEASED message to your program, which the api will receive in its internal message loop. It will then translate this into a OS_NOTIFICATION_COMMAND notification which is sent to the button's notification callback. All the controls that need to be repainted set the repaint flag in their header, and the repaint-descendent flag in all their parent gui objects. Before the api processes the next message from the window manager, it will then do the actual repainting. It will find all the controls with the repaint flag set, and draw them to the window's frame buffer. Once everything is painted to the frame buffer, it will tell the window manager to redraw the modified areas of the frame buffer to the screen.
I do not wait until the message queue is empty to repaint, because I think it will make the program more responsive, but I'm not sure this is best strategy..?

Also my question/suggestion was that I would like to have a unified way of processing messages (system and user messages in the same place) without using a callback so that I control when and how it's happening.

I'm not sure I want it to work this way. There is a lot of special handling that the api does in its message loop that I wouldn't want to make the users do. However I will consider changing it, as you have suggested it.
But in my opinion, the message loop is part of the platform, not the program.

Could you show (in pseudo code) a game loop for your os ?

When you receive the OS_MESSAGE_CREATE_INSTANCE message, call OSSendIdleMessages(true). Then when you receive OS_MESSAGE_IDLE messages you can update your game, using the microsecondsSinceLastIdleMessage field in the OSMessage structure. You can also limit the framerate by calling OSSleep(uint64_t milliseconds).
#15889
Simon Anciaux  —  2 weeks, 4 days ago
nakst
I do not wait until the message queue is empty to repaint, because I think it will make the program more responsive, but I'm not sure this is best strategy..?
I think it's making it less responsive.
If you have 3 messages in the queue, the duration to get the final frame is 3 * message processing + 3 * rendering, with rendering being generally heavier than message processing and the intermediate state probably won't be visible unless you flip the frame buffer each time (and in that case, with vsync you would have a lot of lag). If you process the 3 messages at the same time the duration is 3 * message processing + 1 * rendering.
nakst
There is a lot of special handling that the api does in its message loop that I wouldn't want to make the users do. However I will consider changing it, as you have suggested it.
As a user I don't want to have to do a lot to get messages. If the OS can create a message queue that I can consume as I want, the special handling could still be done by the OS ?
nakst
But in my opinion, the message loop is part of the platform, not the program.
I disagree with that. I think the while(PeekMessage( )) loop is simple enough and let me be the one controlling the program as opposed to being a service to the OS API. I can sort of see how your way of doing things can work for simple applications. Maybe two level of API would work, the "low" level with the PeekMessage loop, and the "high" level with the processNotification callback ?
nakst
Then when you receive OS_MESSAGE_IDLE messages you can update your game, using the microsecondsSinceLastIdleMessage field in the OSMessage structure. You can also limit the framerate by calling OSSleep(uint64_t milliseconds).
I suppose there will be no rendering between messages (I guess rendering is only for the OS gui layer). But this way of handling messages for a game seems weird to me. And a sleep function with a granularity of milliseconds is useless for games in my opinion (not that I use Sleep in games).
Log in to comment