WIP: Equation editing #1

Draft
Heath123 wants to merge 9 commits from Heath123/TeX:equedit into master
First-time contributor

So far it's just cursor movement, and the cursor has to be drawn by the calling application. It also has to handle keypresses, e.g.:

#include "gint/display-cg.h"
#include "gint/keycodes.h"
#include <gint/display.h>
#include <gint/keyboard.h>

#include <TeX/TeX.h>
#include <TeX/env.h>
#include <TeX/flow.h>
#include <TeX/node.h>
#include <TeX/edit.h>

#include <stdbool.h>
#include <stdio.h>
#include <string.h>

struct TeX_Env_Primary
{
	struct TeX_Env env;

	struct TeX_Flow *flow;
};

void pixel(int x, int y, int color) {
	dpixel(x, y, color);
}
void line(int x1, int y1, int x2, int y2, int color) {
	dline(x1, y1, x2, y2, color);
}
void size(char const *str, int *w, int *h) {
	dsize(str, NULL, w, h);
}
void text(char const *str, int x, int y, int color) {
	dtext(x, y, color, str);
}

struct editContext editContext;

int main(void) {
	TeX_intf_pixel(pixel);
	TeX_intf_line(line);
	TeX_intf_size(size);
	TeX_intf_text(text);

	char* code = "\\frac{x^7\\left[X,Y\\right]+\\left|\\frac{A}{B}\\right>}{\\left\\{\\frac{a_k+b_k}{k!}\\right\\}^5}+\\int_a^bt dt+\\prod_{i=1}^{n}x_iy_i+\\sum_{i=1}^{n}\\vec{AB}+\\lim_{x \\to 2}f(x)+\\left(\\begin{matrix} \\frac{1}{2} & 5 \\\\ -1 & a+b \\end{matrix}\\right)";
	struct TeX_Env* formula = TeX_parse(code, 1);
	if (strcmp(formula->name, "primary")) {
		dclear(C_WHITE);
		dtext(1, 1, C_RED, "Non-primary environment");
		getkey();
		TeX_free(formula);
		return 0;
	}
	struct TeX_Env_Primary* formula_primary = (struct TeX_Env_Primary*) formula;

	editContext.elementIsText = false;
	editContext.cursorFlow = formula_primary->flow;
	editContext.offset = 0;

	while (1) {
		dclear(C_WHITE);
		TeX_draw(formula, &editContext, 8, 8, C_BLACK);

		line(
			editContext.cursorX,
			editContext.cursorY,
			editContext.cursorX,
			editContext.cursorY + 8,
			C_RED
		);

		dupdate();
		key_event_t key = getkey();
		if (key.key == KEY_LEFT) {
			TeX_flow_cursor_action(formula_primary->flow, &editContext, CURSOR_MOVE_LEFT);
		} else if (key.key == KEY_RIGHT) {
			TeX_flow_cursor_action(formula_primary->flow, &editContext, CURSOR_MOVE_RIGHT);
		} else if (key.key == KEY_MENU || key.key == KEY_EXIT) {
			break;
		}

	}

	TeX_free(formula);

	return 1;
}

Note that this includes breaking API changes. Also, there are many things in here that aren't really done, and the cursor navigates through some things in an odd order and it doesn't work with matrices (it just skips over them as if they were a single node). Also some of my code might not fit the code style but I made it mostly fit.

So far it's just cursor movement, and the cursor has to be drawn by the calling application. It also has to handle keypresses, e.g.: ```c #include "gint/display-cg.h" #include "gint/keycodes.h" #include <gint/display.h> #include <gint/keyboard.h> #include <TeX/TeX.h> #include <TeX/env.h> #include <TeX/flow.h> #include <TeX/node.h> #include <TeX/edit.h> #include <stdbool.h> #include <stdio.h> #include <string.h> struct TeX_Env_Primary { struct TeX_Env env; struct TeX_Flow *flow; }; void pixel(int x, int y, int color) { dpixel(x, y, color); } void line(int x1, int y1, int x2, int y2, int color) { dline(x1, y1, x2, y2, color); } void size(char const *str, int *w, int *h) { dsize(str, NULL, w, h); } void text(char const *str, int x, int y, int color) { dtext(x, y, color, str); } struct editContext editContext; int main(void) { TeX_intf_pixel(pixel); TeX_intf_line(line); TeX_intf_size(size); TeX_intf_text(text); char* code = "\\frac{x^7\\left[X,Y\\right]+\\left|\\frac{A}{B}\\right>}{\\left\\{\\frac{a_k+b_k}{k!}\\right\\}^5}+\\int_a^bt dt+\\prod_{i=1}^{n}x_iy_i+\\sum_{i=1}^{n}\\vec{AB}+\\lim_{x \\to 2}f(x)+\\left(\\begin{matrix} \\frac{1}{2} & 5 \\\\ -1 & a+b \\end{matrix}\\right)"; struct TeX_Env* formula = TeX_parse(code, 1); if (strcmp(formula->name, "primary")) { dclear(C_WHITE); dtext(1, 1, C_RED, "Non-primary environment"); getkey(); TeX_free(formula); return 0; } struct TeX_Env_Primary* formula_primary = (struct TeX_Env_Primary*) formula; editContext.elementIsText = false; editContext.cursorFlow = formula_primary->flow; editContext.offset = 0; while (1) { dclear(C_WHITE); TeX_draw(formula, &editContext, 8, 8, C_BLACK); line( editContext.cursorX, editContext.cursorY, editContext.cursorX, editContext.cursorY + 8, C_RED ); dupdate(); key_event_t key = getkey(); if (key.key == KEY_LEFT) { TeX_flow_cursor_action(formula_primary->flow, &editContext, CURSOR_MOVE_LEFT); } else if (key.key == KEY_RIGHT) { TeX_flow_cursor_action(formula_primary->flow, &editContext, CURSOR_MOVE_RIGHT); } else if (key.key == KEY_MENU || key.key == KEY_EXIT) { break; } } TeX_free(formula); return 1; } ``` Note that this includes breaking API changes. Also, there are many things in here that aren't really done, and the cursor navigates through some things in an odd order and it doesn't work with matrices (it just skips over them as if they were a single node). Also some of my code might not fit the code style but I made it mostly fit.
Heath123 added 1 commit 2023-01-27 15:55:56 +01:00
Heath123 added 1 commit 2023-01-27 15:58:48 +01:00
Heath123 added 1 commit 2023-01-27 16:00:42 +01:00
Heath123 added 1 commit 2023-01-29 15:02:37 +01:00
Heath123 added 1 commit 2023-01-29 15:14:16 +01:00
Heath123 added 1 commit 2023-01-29 15:21:23 +01:00
Heath123 added 1 commit 2023-01-29 15:36:19 +01:00
Lephenixnoir reviewed 2023-01-30 10:00:29 +01:00
src/node.c Outdated
@ -18,0 +40,4 @@
else if (action == CURSOR_MOVE_RIGHT) {
// We take away 2 because offset 0 is actually one character into the text,
// and the last possible offset is one character before the end of the text,
// because if the cursor is at the start or end it goes in the parent flow.
Owner

Note that if you don't allow cursor positions at the start and end of text nodes, you will need extra logic to decide where to insert text when typing. Specifically, if the cursor is in a flow, you won't be able to just insert a new text node, you'll have to check whether there is a text node nearby.

I'd personally rather do the opposite, ie. move the cursor at the extreme positions of text nodes while skipping the corresponding inter-node positions in the flow. I can also see it helping when inserting other nodes (eg. inserting a fraction and grabbing parts of a text node with it), though that can surely be debated.

Note that if you don't allow cursor positions at the start and end of text nodes, you will need extra logic to decide where to insert text when typing. Specifically, if the cursor is in a flow, you won't be able to just insert a new text node, you'll have to check whether there is a text node nearby. I'd personally rather do the opposite, ie. move the cursor at the extreme positions of text nodes while skipping the corresponding inter-node positions in the flow. I can also see it helping when inserting other nodes (eg. inserting a fraction and grabbing parts of a text node with it), though that can surely be debated.
Author
First-time contributor

So I guess I could add another return type which is "not only did the cursor leave this node, but it needs to skip a whole other space too" and have the parent flow do that

So I guess I could add another return type which is "not only did the cursor leave this node, but it needs to skip a whole other space too" and have the parent flow do that
Owner

Hmm I'd say it's better to not have the child node make decisions about its parent, which might not even exist. Instead, I'd add code in the flow function saying that if we just exited a text node, we skip the adjacent in-between-nodes positions in the flow.

Hmm I'd say it's better to not have the child node make decisions about its parent, which might not even exist. Instead, I'd add code in the flow function saying that if we just exited a text node, we skip the adjacent in-between-nodes positions in the flow.
Author
First-time contributor

Well if the recursive function is being called, something is calling it, right? So it can choose to ignore the value, but it wants to observe it it can. At least, that's how my current code works with the CURSOR_PAST_END logic

Well if the recursive function is being called, something is calling it, right? So it can choose to ignore the value, but it wants to observe it it can. At least, that's how my current code works with the CURSOR_PAST_END logic
Owner

It could be called by the user directly because the node is top-level. What I mean is each node should behave as if it was isolated, because such design (when applicable) makes it easier to keep the information flow clean.

Ultimately it's just a subtle difference of looking at the chain of tests and decisions that the program will make, and deciding where recursive calls and returns should occur, ie. which node/flow is responsible for making each decision.

In this case I believe the behavior at the end of text nodes is a quirk of flows, not a fundamental property of the model. It wouldn't make sense for a fraction to return "right and skip one place". Hence, I believe it's cleaner to change the flow function than then API for these functions.

(These are fairly small details and neither option would completely break the code structure. I just think it's worth exploring the details once in a while, as these are rarely discussed but still important when building clean interfaces. Choosing the best option every times adds up eventually!)

It could be called by the user directly because the node is top-level. What I mean is each node should behave as if it was isolated, because such design (when applicable) makes it easier to keep the information flow clean. Ultimately it's just a subtle difference of looking at the chain of tests and decisions that the program will make, and deciding where recursive calls and returns should occur, ie. which node/flow is responsible for making each decision. In this case I believe the behavior at the end of text nodes is a quirk of flows, not a fundamental property of the model. It wouldn't make sense for a fraction to return "right and skip one place". Hence, I believe it's cleaner to change the flow function than then API for these functions. (These are fairly small details and neither option would completely break the code structure. I just think it's worth exploring the details once in a while, as these are rarely discussed but still important when building clean interfaces. Choosing the best option every times adds up eventually!)
Author
First-time contributor

I was thinking that if user code is calling this function then it's acting as a parent, and may be managing the cursor itself in the same way (e.g. navigating between multiple equations) so this information could be valuable to it.

It does feel a little hacky to have a separate return value like that, but then it feels even more hacky to have the parent have a special case in the code for a certain node type. For example, what if a new type of text node is added that can handle e.g. taller characters, and needs to behave the same way? It seems like it would be cleaner to have a way to specify that behaviour for any node that behaves that way than it would to check this in the parent...

My mental model of this is that any information required for the parent to manage the cursor movement when it moves out of a node flows up the tree, which isn't so much the node no longer being isolated as the parent and child working together to set the node to the correct position, which is necessary anyway as the child doesn't have enough information to do it on its own. If there is no parent then that's fine as the position the child leaves the cursor in before it returns is the correct and expected position if there is no parent to move it to, which in this case is the start or end of the text node.

If you're sure that the special case way is cleaner then I will do it but it feels less clean to me. I thought the cleanest way would be the original thing where it exits to the flow which is why I did that originally

I was thinking that if user code is calling this function then it's acting as a parent, and may be managing the cursor itself in the same way (e.g. navigating between multiple equations) so this information could be valuable to it. It does feel a little hacky to have a separate return value like that, but then it feels even more hacky to have the parent have a special case in the code for a certain node type. For example, what if a new type of text node is added that can handle e.g. taller characters, and needs to behave the same way? It seems like it would be cleaner to have a way to specify that behaviour for any node that behaves that way than it would to check this in the parent... My mental model of this is that any information required for the parent to manage the cursor movement when it moves out of a node flows up the tree, which isn't so much the node no longer being isolated as the parent and child working together to set the node to the correct position, which is necessary anyway as the child doesn't have enough information to do it on its own. If there is no parent then that's fine as the position the child leaves the cursor in before it returns is the correct and expected position if there is no parent to move it to, which in this case is the start or end of the text node. If you're sure that the special case way is cleaner then I will do it but it feels less clean to me. I thought the cleanest way would be the original thing where it exits to the flow which is why I did that originally
Owner

That's a fair analysis. The big question then is: does that behavior generalizes to enough nodes to be considered part of the model?

I believe it's specific to text nodes because skipping a position in the flow only makes sense when two conditions are met: (1) visual text alignment, and (2) semantic equivalence.

(2) refers to the fact that the formula itself still means the same thing regardless of whether we insert at the end of a text node or just after it. This is a pretty rare thing, for instance if I have a formula 2^3 and the cursor before the 2 I really care about whether I'm inserting inside the base or outside of it. Visually it is similar (and confusing), but fundamentally the insertions are quite different, so the cursor should allow both to happen. I think text nodes are the only case where this will happen because they are the only nodes to not have a mathematical interpretation.

(1) refers to the fact that if the two positions being considered (end of text and inside the flow) are not visually identical (up to a couple pixels), the mechanic will be confusing/wrong. Inserting at two different places cannot reasonably be understood to be the same thing (and math notation usually follows that principle). That means the decision to skip a cursor position must depend on some layout, which is node- and flow-specific, so it makes little sense to me to have it as a general response.

One last thought: an alternative to "move right and skip a space" might be "move right" along with "btw, the rightmost cursor position of this node type can be shared with the parent".

Anyway, no bikeshedding there. Feel free to choose whatever solution you think is the cleanest. I just wanted to dive in this question as a design exercise, which I'm sure we've achieved now. x3

That's a fair analysis. The big question then is: does that behavior generalizes to enough nodes to be considered part of the model? I believe it's specific to text nodes because skipping a position in the flow only makes sense when two conditions are met: (1) visual text alignment, and (2) semantic equivalence. (2) refers to the fact that the formula itself still means the same thing regardless of whether we insert at the end of a text node or just after it. This is a pretty rare thing, for instance if I have a formula `2^3` and the cursor before the 2 I really care about whether I'm inserting inside the base or outside of it. Visually it is similar (and confusing), but fundamentally the insertions are quite different, so the cursor should allow both to happen. I think text nodes are the only case where this will happen because they are the only nodes to not have a mathematical interpretation. (1) refers to the fact that if the two positions being considered (end of text and inside the flow) are not visually identical (up to a couple pixels), the mechanic will be confusing/wrong. Inserting at two different places cannot reasonably be understood to be the same thing (and math notation usually follows that principle). That means the decision to skip a cursor position must depend on some layout, which is node- and flow-specific, so it makes little sense to me to have it as a general response. One last thought: an alternative to "move right and skip a space" might be "move right" along with "btw, the rightmost cursor position of this node type can be shared with the parent". Anyway, no bikeshedding there. Feel free to choose whatever solution _you_ think is the cleanest. I just wanted to dive in this question as a design exercise, which I'm sure we've achieved now. x3
Author
First-time contributor

I've realised that my solution isn't really any cleaner because there still has to be special logic in the parent for entering the nodes, so I'll just do it your way (when I get back to working on this)

I've realised that my solution isn't really any cleaner because there still has to be special logic in the parent for _entering_ the nodes, so I'll just do it your way (when I get back to working on this)
Owner

What special logic is that? I'd expect that entering a new node is a simple call to TeX_node_cursor_enter(), which would not be specific to any type of node (unlike my proposition which needs to add logic specific to text nodes).

What special logic is that? I'd expect that entering a new node is a simple call to `TeX_node_cursor_enter()`, which would not be specific to any type of node (unlike my proposition which needs to add logic specific to text nodes).
Author
First-time contributor

Well the special logic is because it needs to enter the node earlier than it normally would. With a normal node like a fraction, if it starts like this:

Then you would move the cursor one to the right, between the two nodes:

Then once again to enter the node:

For text nodes you start like this (pretend that the 1 is its own node for some reason):

And then when you go right again you want to immediately capture the cursor:

Without special logic, the cursor would instead be in between the 1 and the text node.

The child can't handle this capturing with the current structure, because the cursor is in the parent at this point so the child isn't even recursed into. So there has to be special logic in the parent anyway. Though I haven't worked on this for a while so I may be forgetting something here

(these screenshots aren't from the calculator, I haven't gotten antialiased fonts working yet)

Well the special logic is because it needs to enter the node earlier than it normally would. With a normal node like a fraction, if it starts like this: ![](https://cdn.discordapp.com/attachments/740606937477152790/1078795970181074945/image.png) Then you would move the cursor one to the right, between the two nodes: ![](https://cdn.discordapp.com/attachments/740606937477152790/1078796410742382722/image.png) Then once again to enter the node: ![](https://cdn.discordapp.com/attachments/740606937477152790/1078796599964217414/image.png) For text nodes you start like this (pretend that the 1 is its own node for some reason): ![](https://cdn.discordapp.com/attachments/740606937477152790/1078797038684217454/image.png) And then when you go right again you want to immediately capture the cursor: ![](https://cdn.discordapp.com/attachments/740606937477152790/1078797360664158308/image.png) Without special logic, the cursor would instead be in between the 1 and the text node. The child can't handle this capturing with the current structure, because the cursor is in the parent at this point so the child isn't even recursed into. So there has to be special logic in the parent anyway. Though I haven't worked on this for a while so I may be forgetting something here (these screenshots aren't from the calculator, I haven't gotten antialiased fonts working yet)
Author
First-time contributor

I made it work like you said, but now the program has to do this check after each action:

// If the cursor is next to a text node, put the cursor in the text node
if (!editContext.elementIsText) {
	// Flows store nodes in a linked list so iterate to get the one at the offset
	struct TeX_Node* nodeBefore;
	struct TeX_Node* nodeAfter;
	if (editContext.offset == 0) {
		nodeBefore = NULL;
		nodeAfter = editContext.cursorFlow->first;
	} else {
		struct TeX_Node* node = editContext.cursorFlow->first;
		for (int i = 0; i < editContext.offset - 1; i++) {
			node = node->next;
		}
		nodeBefore = node;
		nodeAfter = node->next;
	}

	if (nodeBefore != NULL && nodeBefore->type == 0) {
		editContext.elementIsText = true;
		editContext.cursorText = nodeBefore;
		editContext.offset = strlen(nodeBefore->text);
	} else if (nodeAfter != NULL && nodeAfter->type == 0) {
		editContext.elementIsText = true;
		editContext.cursorText = nodeAfter;
		editContext.offset = 0;
	}
}

I'll make the library do this for you

I made it work like you said, but now the program has to do this check after each action: ```c // If the cursor is next to a text node, put the cursor in the text node if (!editContext.elementIsText) { // Flows store nodes in a linked list so iterate to get the one at the offset struct TeX_Node* nodeBefore; struct TeX_Node* nodeAfter; if (editContext.offset == 0) { nodeBefore = NULL; nodeAfter = editContext.cursorFlow->first; } else { struct TeX_Node* node = editContext.cursorFlow->first; for (int i = 0; i < editContext.offset - 1; i++) { node = node->next; } nodeBefore = node; nodeAfter = node->next; } if (nodeBefore != NULL && nodeBefore->type == 0) { editContext.elementIsText = true; editContext.cursorText = nodeBefore; editContext.offset = strlen(nodeBefore->text); } else if (nodeAfter != NULL && nodeAfter->type == 0) { editContext.elementIsText = true; editContext.cursorText = nodeAfter; editContext.offset = 0; } } ``` I'll make the library do this for you
Owner

Is that really a good idea? I'd rather go into text nodes when text gets input instead of at every action. If you edit in a way that adds a node, like inserting a fraction, sum, etc, I think it's better to have the cursor be in the flow.

Is that really a good idea? I'd rather go into text nodes when text gets input instead of at every action. If you edit in a way that adds a node, like inserting a fraction, sum, etc, I think it's better to have the cursor be in the flow.
Author
First-time contributor

When you've navigating through the nodes there is no input, so it needs to either do this, or go back to way of doing it I had before where the cursor wasn't allowed to be at the start and end, don't I?

I could make it always follow this rule except when a node has just been added but that seems like like it would cause problems with the only advantage being having to do one less check sometimes, so I think it's better if it's consistent with always being in a text node if it's at the start or end. Or am I misunderstanding what you're saying?

When you've navigating through the nodes there is no input, so it needs to either do this, or go back to way of doing it I had before where the cursor wasn't allowed to be at the start and end, don't I? I could make it always follow this rule except when a node has just been added but that seems like like it would cause problems with the only advantage being having to do one less check sometimes, so I think it's better if it's consistent with always being in a text node if it's at the start or end. Or am I misunderstanding what you're saying?
Owner

Consistency is definitely better, that's for sure. I think I'm just not clear on the fine details of your model. Let's forget my remark, as long as you have a clear set of valid cursor positions with no "duplicates" (ie. you don't have both offset 0 in a text node and in a flow before that node) and the cursor position is always one of them, you're definitely fine.

Consistency is definitely better, that's for sure. I think I'm just not clear on the fine details of your model. Let's forget my remark, as long as you have a clear set of valid cursor positions with no "duplicates" (ie. you don't have both offset 0 in a text node and in a flow before that node) and the cursor position is always one of them, you're definitely fine.
Owner

That's really nice! This is exactly how to use the recursive method. Other than the search which is a bit unnatural (but not easy to avoid as we discussed), there isn't much to improve I think. Maybe naming/ordering/etc with the API later when we merge the whole thing. One question: why did you not draw the cursor directly in the rendering functions?

That's really nice! This is exactly how to use the recursive method. Other than the search which is a bit unnatural (but not easy to avoid as we discussed), there isn't much to improve I think. Maybe naming/ordering/etc with the API later when we merge the whole thing. One question: why did you not draw the cursor directly in the rendering functions?
Author
First-time contributor

One question: why did you not draw the cursor directly in the rendering functions?

Currently, the left brackets overlap the cursor, so I needed the cursor to always be drawn at the end on top of them and in a different colour. This could be fixed by adjusting the cursor to not overlap things but this seemed like the easiest way to fix it and maybe it could allow for more customisation

> One question: why did you not draw the cursor directly in the rendering functions? Currently, the left brackets overlap the cursor, so I needed the cursor to always be drawn at the end on top of them and in a different colour. This could be fixed by adjusting the cursor to not overlap things but this seemed like the easiest way to fix it and maybe it could allow for more customisation
Heath123 force-pushed equedit from 74d94a3979 to 12b8599370 2023-02-25 18:54:31 +01:00 Compare
Heath123 added 1 commit 2023-03-03 14:26:06 +01:00
This pull request is marked as a work in progress.
This branch is out-of-date with the base branch
You can also view command line instructions.

Step 1:

From your project repository, check out a new branch and test the changes.
git checkout -b Heath123-equedit master
git pull equedit

Step 2:

Merge the changes and update on Forgejo.
git checkout master
git merge --no-ff Heath123-equedit
git push origin master
Sign in to join this conversation.
No reviewers
No Label
No Milestone
No Assignees
2 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: Lephenixnoir/TeX#1
No description provided.