This looks great. I've been impressed at how flexible CodeMirror is though. When I started EtherPK (https://app.etherpk.com) I made a bet that CodeMirror could help me build an editor that could combine outliner blocks, standard prose and rich overlays / graphics / embeds in one editor. I was concerned that I'd hit a road block (I presumed Logseq used custom HTML, nested divs and click position mapping for rendered text to the underlying input for a reason), but 9 months in I couldn't be happier with my choice, and it's a blazing fast editor that feels really nice and light to type in (live demo: https://app.etherpk.com/demo)
I wish Office 365 would hire someone like the author. Online Word never renders the same as it would in the Desktop software...and this matters. I deal with only modestly complex documents when I write grants but I do need to stay within page length limits for sections, I need some layout stability...so I inevitably fire up my Windows VM to run the application.
Before anyone suggests LateX, I know, I know, but when you write collaboratively, you don't always get to choose the platform.
Google has switched in 2020 (if i am not mistaken to canvas), but I am not saying this would be the solution to everything, It is just another option one could do. I do not know if someone noticed this, but that is part of the reason you could not just open google docs on mobile. Because I would suspected that it would been too much effort for them back than. In anyway Tasfer tried as best as possible to do that on mobile too.
I wrote a similar tool to this many years ago that used JATS as an internal document model, using contentEditable, and I can relate to the pain you refer to. Getting the semantics right in cases where you split or join text that has varying types of nested context was hard.
I like what you did here in terms of the cursor that points one way or the other: it tells you if you're in "normal" mode, or for example, in a bold region. I don't think I've seen that before so, if you made it up, that itself is a cool invention.
That's a really nice invention. In general I much enjoy the editor, it's refreshing how it operates, it has many good ideas. But it's not working as it should? How to delete the bold markup, for example? Can't get it done without using the Ctrl+B shortcut, whereas I could create it by typing markdown. When I delete the first bullet, undo doesn't restore it. When i delete the second, undo jumped the cursor to the first bullet.. So there's work to be done still. When copying the text, I'd like to get the markdown text, not some sterilized version of it (leave that to Shift+Ctrl+V if it's needed).
And that’s a part of why I said the approach is bad: there’s a long tail of native features that depend on being able to see/interact with more than just the current word or selection or whatever, and you’re thwarting that. You cannot make a lot of them work, and the more effort you put into trying to cover them, the more you end up with something even unholier than contenteditable.
Give up. Seriously, please. Switch to DOM, it is genuinely mandatory for acceptable results.
You can get close enough on desktop that many users won’t notice, but you emphatically cannot reach even tolerable on mobile.
We should chat (check bio)! For almost a decade now I’ve been working on a writing tool that might benefit from it. I build occasional forks with procgen on top it. That being said better bidi handling is something I’m most interested in.
Enso.sonnet.io - what I’m talking about.
(On untested.sonnet.io you’ll find notes about the experiments / toys related)
Ok the custom rich text latex editor is really cool. But I'm on a tablet and stuff like selecting text is a bit awkward (the up down auto scroll is not exactly the same as the native one and isn't as precise as the native one).
how are you handling IME and screen readers with canvas? also do you relayout per line or per block? only relaying out changed blocks was the biggest typing speedup in my markdown editor
Cool idea! I approve of the privacy oriented design. But it looks like you didn’t actually test this on iOS as automatic text correction is completely broken.
Also your website appears to have been written and generated entirely with an LLM. That doesn’t inspire trust.
There’s no better way to get a lot of testers than to have a Show HN.
I’ll confirm - there are cursor bugs on iOS. Both on newlines and autocorrect.
I’m personally more forgiving of an LLM “helped” site, if it looks like there was a human in the loop for the process. An LLM has more context about the design goals and architecture than the reader, so if it’s an informative page, I’m not personally bothered.
A human is needed to show how app’s technical features are relevant to a particular audience with a shared social context. As the LLM is not (and should not ever be) a participant in that social environment, any marketing it generates will almost certainly land as tone deaf or presumptuous.
I take advice and feedback like yours seriously, thank you for giving your opinion, I appreciate it. For iOS, I tried to make it work, but iOS has really made it tough in that regard. For some reason they do not like a hidden contenteditable controlled by a canvas. That is part of the reason I have an iOS app published on the App Store.
Also, I know that LLMs might sound wrong to many, and I get where this is coming from.
For me, AI gives me, as one lone developer, the power to create and implement these ideas. It helped me translate and do so much more. I agree this may create a feeling of distrust, and I don't think it should not replace thinking.
iOS was telling you “don’t do this, it won’t work”, and your answer was to ignore it, ship something broken, and make an app for certain platforms as well.
The LLM will happily help you to implement the wrong thing. It’s not good at saying “you can try doing this, but the result will never be good”.
This is a good demonstration of why you really shouldn’t do this. Bluntly: this is completely unusable for many people, barely usable for a lot more, and on desktop and mobile both I would absolutely hate to use this, and a lot of the problems in it fundamentally cannot be fixed.
> But it also means the editor looks and behaves the same in every browser, on desktop and on phone, because nobody else is in charge of the pixels.
You say this as though it were a good thing. It’s very much not; they’re not supposed to be the same, they have slightly different expected renderings, and significantly different interactions.
And you can’t inspect or emulate them.
—⁂—
First of all, Firefox under Linux. I see it renders with my chosen fonts, indicating (since you’ve said you’re using 2D canvas) that you’re using fillText. Good; I would complain more about the alternative. But it does kinda go against a lot of what you said about who was in charge of the pixels.
I use my Compose key. Underlined · placeholders, proper replacement, &c. Huh, you’ve surprised me by actually handling that correctly. Well done.
Though I do note that when I type U+1F928 FACE WITH ONE EYEBROW RAISED in a title, in the app’s sidebar and top-of-page title it becomes U+1F929 GRINNING FACE WITH STAR EYES. That’s… not encouraging, but ultimately presumably Just a Bug.
I switch to Telugu and type “telugu”, తెలుగు. Uh oh… with each new consonant it drops the completed syllable, and when I press Escape at any point it deletes the composition. Utterly debilitating bug for IME usage, but ultimately fixable.
I enter some text, find that for the most part it works significantly better than I expected, even handling things like clipboards pretty well.
Then I turn to what I know it will fail, because it’s not possible to get correct universally (different platforms have different behaviours and you can’t probe it; user-agent/platform sniffing would get the best results): keyboard navigation. Ugh, Ctrl+{Left,Right} is utterly wrong. I expected it to have implemented perhaps Windows’ behaviour and I’d complain about that, but no, it’s stopping at both the end of one word and the start of the next, which no platform has ever done. Ctrl+{Up,Down} doesn’t work by paragraphs but still by line. PageUp/PageDown kinda work but the amounts they move by are just wrong, very wrong. And so on. Cursors are frustratingly non-native the while (becomes move when hovering over selected text).
I check whether Backspace seems sane on Telugu text. Seems to be, to my satisfaction. Ctrl+Backspace, of course, is utterly wrong like Ctrl+Left was.
I decide to skip testing any BiDi. But I do run through some of https://lord.io/text-editing-hates-you-too/, and watch it fail vertical cursor movement, affinity, emoji modifiers… meh, BiDi is here, so I decide to test it once and oh dear while it supports the general concepts properly (including selection) it seems to think that if you give it a little bit of RtL in an LtR paragraph it should turn the paragraph RtL which is badly wrong, and then it handles other aspects of selection and keyboard interaction badly wrongly. And it already failed composition. So yeah… it failed them all, where Firefox handles them all correctly.
I try zooming with my touchpad. Nothing happens if the cursor is over the canvas. Grr. Works but leaves the canvas blurry if the cursor is outside it. (Fixable.)
I paste the full text of Winnie-the-Pooh <https://www.gutenberg.org/cache/epub/67098/pg67098-images.ht...>. It locks up the browser tab for several seconds. (Not fundamental, but not surprising either. The browser can handle pasting a mere 26,000 words with light markup in an instant.) And takes perhaps minutes more in the background to decide it is failing to upload all the images (and regularly goes and tries to load them again later). It also inserts blank paragraphs between every paragraph and turns italics into underscore wrapping for no apparent reason. And mangles “*** START OF THE PROJECT GUTENBERG EBOOK WINNIE-THE-POOH ***” into italic “\\\\\\ START OF … \\\\\*”. Not encouraging, though once again not particularly about canvas text editing.
Now the document is long enough, I try scrolling. </i>Oh no.* Scrolljacking!? That was completely unforced error! You must use a real scrolling area. Scrolling is now utterly tedious for me, because it’s not possible to implement scrolling yourself from the events exposed to you, and whatever monstrosity it’s doing instead gets the scroll rate wildly wrong and lacks inertia, so a full swipe on the touchpad gets almost a pageful instead of several plus a hundred more if done fast. Combined with it hiding its fake scrollbar until you scroll, large documents are nigh unusable. Now once again, this is not fundamental to canvas text editing, so long as you’re willing to put the thing in a proper scroll area that you maintain.
I switch to Firefox for Android with the FUTO keyboard. Initially things seem to be working well, better than I expected, particularly given how I’ve observed the backing text input is generally only being given a word at a time—I think FUTO must be remembering words itself for its suggestions, working around how badly broken what it’s being given is. But then I try to shift the cursor by sliding my finger along the Space key, and it doesn’t work. I switch to something else and come back, and selection is shot, it’s lost the caret and I don’t know what happened to it, and various keyboard interactions are broken. I press and hold on a word to select it as always works, and… it doesn’t: no, it seems like they’ve implemented something a little more like my vague memories of iOS from when I’ve seen that; and badly at that. Correct selection is rendered tedious, unreliable, and sometimes impossible. And as for doing anything with that selection… it offers Copy, Cut, Paste. Utterly inadequate, stopping the browser from doing stuff with the text (e.g. share to another app), but unavoidable in the canvas approach.
At this point I give up; it’s completely unusable for anything but the basics. A lot of what I complained about is just that this is buggy and incomplete, but a lot of it is also fundamentally unfixable.
Even the buggy and incomplete parts indicate why the approach is, to put it gently, risky. But the unfixable parts—
This is why you should only use contenteditable.
And I didn’t even get to throttling my CPU to 400MHz to test performance. But it’s after half past midnight and I should be asleep already.
This is really valuable! Just to add a bit of context: I'm not the only one using canvas for rendering. Google Docs did it years before me. Think of it this way: I've built this almost entirely alone and still get most of the basics working.
Let's start with your test. I think the reason it struggled with the Gutenberg text is that it had a lot of images. Either way, that's a bug and it should be fixed (it shouldn't load all the images at once, and there's probably too much work happening on the main thread). I really appreciate the effort you put in.
I could also use a WebGPU or WebGL context, which would certainly be faster. I think I had a working version last year, but it had problems with blurry rendering (font hinting). I don't remember the exact details now.
Overall, I'm really thankful that you pointed these out. I'll try to work through them over the next few days. I do disagree with the premise, though: canvas is more than capable of achieving this. Performance shouldn't be an issue. That said, I agree with your points about integration with context menus and other tooling.
I have pointed out various fixable bugs, but I also pointed out some critical things that are fundamentally unfixable, and make the entire approach unbearable. You cannot make it acceptable on a pretty large fraction of mobile devices, and even where it’s not horribly broken, it’s still radically different from every single other thing on the device in extraordinarily frustrating ways.
As for Google Docs, well, I’ll just link to what I’ve written about it before: https://news.ycombinator.com/item?id=42253177. I honestly believe they lied in their justification, because the resulting experience is genuinely worse in every way. I can’t comment on how it is on mobile, because I’m too lazy to log in, and they probably do something different on mobile anyway.
What choice do we have? We have to find a solution to the problem. ContentEditable is much harder to work with. This is a solution, and I hope we will at some point work out a new web API that makes these tasks easier.
Your reasoning is that I am missing out on a large percentage of users. I understand where this is coming from. I am working on mobile apps as well, as I did with iOS. I can try to close that gap even more, but I can't just switch to ContentEditable. I don't have the time, and there are already tons of projects that do that.
But if Google did it at their scale, then I think the tradeoff is reasonable, don't you think? You said in another HN post that they don't really use pure canvas. What do you mean? ContentEditable for capturing input?
By the time you’ve fixed as much as you can, you will certainly have more code, more complex code and more fragile code than you would have had from massaging contenteditable into your desired shape, and your result won’t even be better than it in any indisputable way.
“Pure canvas” means when the entire app runs inside a canvas to the maximum extent possible, such as with https://www.egui.rs/. Google Docs isn’t pure canvas because it uses regular DOM for the chrome, leaves text rendering to the browser (fillText or whatever) and uses real scrolling area (multiple canvases, tiling). Yours is similarly not pure canvas, though it’s a little closer in the obviously-bad way of how you implement scrolling yourself.
“Google did it” is not an endorsement for others doing it. They have often quite bizarre rationales that truly don’t apply to smaller organisations. As I have written, I’ve thought it through carefully multiple times in multiple ways, and I honestly believe they were lying about their motives through and through, because they just don’t make any sense. Google Docs was unambiguously better before they migrated to canvas, and most of their claims about what it got them were demonstrably false.
> Though I do note that when I type U+1F928 FACE WITH ONE EYEBROW RAISED in a title, in the app’s sidebar and top-of-page title it becomes U+1F929 GRINNING FACE WITH STAR EYES. That’s… not encouraging, but ultimately presumably Just a Bug.
Fun discovery: this is a Firefox bug. Address bar, text areas, &c. and it’s consistently rendering the wrong character, but somehow your canvas got it mysteriously right. Wild! I should update my Nightly (couple of weeks out of date) and see if it’s still happening, and file a bug report if it is.
This looks slick but I think you need a better way to market it.
The 2D canvas part is the norm for anyone who wants performance. Working with the DOM becomes painfully slow and resource intensive the moment you add syntax highlighting, spell checking, and other visual chrome, to large documents.
This is why Monaco (the editor widget behind VSCode) is a canvas. Why terminal emulators built with web technologies use canvas. And so on.
So I wouldn’t say the canvas part of the heading is a differentiator.
Edit: apologies, seems I was wildly wrong here. Please disregard the above.
> The 2D canvas part is the norm for anyone who wants performance. Working with the DOM becomes painfully slow and resource intensive the moment you add syntax highlighting, spell checking, and other visual chrome, to large documents.
This is wildly wrong. 2D canvas is consistently slower than letting the browser handle the rendering, so long as you avoid making the DOM too complex, and possibly monitor the viewport and only keep some of the content in the DOM at a time (though for most applications this practice is somewhere between harmful and not useful, and it definitely adds a lot of complexity).
> This is why Monaco (the editor widget behind VSCode) is a canvas.
It isn’t.
> Why terminal emulators built with web technologies use canvas.
Why on earth would you build with web technology, if you’re then going to use canvas? I can’t immediately think of any that match your description (though I have no doubt that in-browser ones using canvas exist).
You’re right. I’ve been experimenting with Monaco lots lately and could have sworn it used a canvas. But it doesn’t.
> 2D canvas is consistently slower than letting the browser handle the rendering, so long as you avoid making the DOM too complex, and possibly monitor the viewport and only keep some of the content in the DOM at a time (though for most applications it’s somewhere between harmful and not useful, and it definitely adds a lot of complexity).
Interesting. I’ve found the reverse to be true. But it sounds like this is human error on my part. (probably doesn’t help that I’ve had more experience building high performance GPU applications in Dx, SDL, and similar than I have building high performance web apps.
Thanks for the correction.
> Why on earth would you build with web technology, if you’re then going to use canvas? I can’t immediately think of any that match your description (though I have no doubt that in-browser ones using canvas exist).
You answered your own question.
And I do know of at least one written using the canvas. Because I wrote it. I’m now interested in how others work though.
Most in-browser terminal emulators just use plain old boring DOM, and ignore the issue of perfect columns. Some detect it and wrap offending characters in fixed-width spans. But the entire concept of columns is unsound, doesn’t work at all for a lot of scripts.
> And there are already definitions and APIs to decide the terminal cell width of different Unicode characters.
And how! I know of four different and mutually-incompatible definitions/behaviours, and I’m confident there will be more; and once you get to things like Indic scripts and Right-to-Left there are no good answers, and no cell-based terminal produces even vaguely acceptable results.
As for meanings of string width, I’d count at least four: Unicode code units, Unicode scalar values, Unicode extended grapheme clusters, and terminal cells.
> And how! I know of four different and mutually-incompatible definitions/behaviours, and I’m confident there will be more;
I’ve not found that to be the case. Usually the issue is more around different libraries being out of date (ie pinned to an earlier version of Unicode)
> and once you get to things like Indic scripts and Right-to-Left there are no good answers, and no cell-based terminal produces even vaguely acceptable results.
That’s because right-to-left isn’t supported and in my opinion shouldn’t be supported my the terminal emulator itself.
Right-to-left support would be handled by the client application, not the terminal emulator. The terminal is just there to offer a grid of Unicode cells. It’s not a writing composition suite.
Just like how the terminal emulator isn’t readline, it isn’t emacs, it isn’t hexdump, and it isn’t vim.
> As for meanings of string width, I’d count at least four: Unicode code units, Unicode scalar values, Unicode extended grapheme clusters, and terminal cells.
I’d already touched on the fact that Unicode widths can vary depending on what you’re measuring. But your Unicode library would/should handle most of the pain there.
When writing software for a terminal, you’re really only interested in the characters you need to loop through, their cell width, and, in some languages, the number of bytes.
As I’ve said previously, I’ve written terminal emulators targeting a variety of different renderers like SDL (so not just js+canvas), as well as complex terminal software like $SHELLs. From what I’ve built over the decades, honestly Unicode support isnt as big problem as it used to be. Sure, there’s plenty of footguns that you need to be aware of, but that’s just the nature of working with technology originally designed in the 60s/70s. Like how you need to be aware of cell widths if you intend to do any manual text alignment in client software. But at least there are libraries to make that task easier.
And if you don’t case about text alignment, then the terminal emulator will handle that for you.
There are several quite different things called wcwidth, in part because as a POSIX function it wasn’t well-specified; now I think there are two main families of it, maybe still three; https://wcwidth.readthedocs.io/en/latest/specs.html is the best-specified and -considered of them, but not the most popular. Then there are others that see how wide a character renders, and quantise in some fashion to cell. (This obviously requires knowledge of the font, which should be unavailable to the shell, so it’s a technique only terminals can use.) And in all of these, there are multiple incompatible versions hanging around. The aforelinked one shows some of its changes over time as they wrestle with trying to get things as reasonable as possible, which is fairly obviously hopeless anyway. (Some characters are just too wide, some extremely so, some less so. U+FDFD is generally made 1×1, but it should truly be something like 2×2 or 1×8 or a little wider depending on whether the font employs https://en.wikipedia.org/wiki/Word_heaping. Or the widest Telugu consonant, y, which wants to be 2 columns by itself or 3 or 4 columns with some conjuncts and vowels (e.g. ykuu, య్కూ), but gets squished to one by itself or two in those wider cases.)
The terminal emulator must handle a lot of RtL stuff to get it acceptable. In raw mode (where dimensions are knowable) you could almost leave it to the app, but outside raw mode it’s pretty obvious that the terminal has to be BiDi-aware. But even in raw mode, you really can’t treat it as you describe, because Arabic text rendering is rather complex, full of necessary ligatures.
So you shouldn't need knowledge of the type-face to know the size of the printed character. If other libraries are printing the characters and then extracting the width from that, then they're doing it wrong and you should look to use a different library instead.
The bigger problem I've run into is not different libraries making up their own width assumptions. It's been libraries built against outdated versions of the Unicode standard.
> The terminal emulator must handle a lot of RtL stuff to get it acceptable. In raw mode (where dimensions are knowable) you could almost leave it to the app, but outside raw mode it’s pretty obvious that the terminal has to be BiDi-aware. But even in raw mode, you really can’t treat it as you describe, because Arabic text rendering is rather complex, full of necessary ligatures.
I think RTL is a distraction from the actual point that we're discussing. But it's worth noting that there are already CSI sequences for enabling RTL behaviour in terminal emulators. It's not something I've personally implemented in my own projects because I have no personal need for it and have had no feature requests from people asking for it who could test the implementation's soundness (unlike Chinese characters, which have seen a reasonable degree of popularity in the projects I've built). But I'm sure some other terminals support it just fine since it was added decades ago as part of ECMA 48.
No, UAX #11 does not define the width of characters. It’s called “East Asian Width” for a reason. As the note in the Scope section begins:
> The East_Asian_Width property is not intended for use by modern terminal emulators without appropriate tailoring on a case-by-case basis. […]
It’s about a very specific typographical custom in a specific set of languages. Terminal emulators have piggy-backed onto it because it’s fairly close to what they want and definitely better than nothing, but if they limit themselves to this definition, they do a bad job.
> you shouldn't need knowledge of the type-face to know the size of the printed character
To see the most extreme example, look through Google Fonts for U+FDFD <https://fonts.google.com/?preview.text=%EF%B7%BD> and see the radical differences in aspect ratio (and size!). In some other scripts there are multiple ways of writing things which have quite different width characteristics. In English we have different accepted and understood letterforms for a, g, v, z and probably others, but they’re all roughly the same width. But this isn’t always true in other scripts. My name is written in Telugu as క్రిస్ (kri s), but that r curve can be drawn below or to the left (both seen among <https://fonts.google.com/?preview.text=%E0%B0%95%E0%B1%8D%E0...>), and even sometimes a little to the right; and you don’t actually have to write it with a conjunct, క్ రిస్ (k ri s) means the same thing and there used to be some fonts or apps that wouldn’t render conjuncts. (Encodingwise, I inserted a ZERO WIDTH NON-JOINER after the {k, suppress vowel} and before the {r, -i} to simulate this effect. And then HN turned the ZWNJ into a space. Sigh. I should ask them once again to stop doing that.) Of these three different legitimate renderings of the same text, one works in two cells, one wants two and a half, one needs three.
> No, UAX #11 does not define the width of characters.
It's literally there in the doc
> It’s called “East Asian Width” for a reason. As the note in the Scope section begins:
Yes. I didn't claim it defines terminal cell widths. There's a reason you need a library to translate that into cell widths. What I said was you don't need to be pratt about with printing because Unicode does define widths and properly libraries pin against that (like POSIX does).
> Terminal emulators have piggy-backed onto it because it’s fairly close to what they want and definitely better than nothing,
That's a very convoluted way of agreeing with me :)
> but if they limit themselves to this definition, they do a bad job.
A large part of the problem here is you're introducing scope creep.
You're shifting goal posts again because you're no longer talking about fixed width typefaces. Terminals have to make some concessions for turning scripts (of any language) into fixed width. If that's an issue then you shouldn't be using the terminal to begin with. There are better standards for that, such as the web.
In short, nobody is going to disagree with you that Unicode is hard and full of edge cases. But the vast majority of the stuff you describe are either already solved, already have standards, or are already out of scope for terminals.
As far as I know, Monaco, the editor behind VS Code, does not use canvas. I noticed that Google Docs uses canvas, and that was the start of my project a year ago. I think that if I had used another context, like WebGL or WebGPU, it would have been much more performant, but it is really hard to render text and parse font files that way.
Anyway, I have gotten good enough results, and I don’t think normal usage will exceed the limits of the 2D canvas.
This looks great. I've been impressed at how flexible CodeMirror is though. When I started EtherPK (https://app.etherpk.com) I made a bet that CodeMirror could help me build an editor that could combine outliner blocks, standard prose and rich overlays / graphics / embeds in one editor. I was concerned that I'd hit a road block (I presumed Logseq used custom HTML, nested divs and click position mapping for rendered text to the underlying input for a reason), but 9 months in I couldn't be happier with my choice, and it's a blazing fast editor that feels really nice and light to type in (live demo: https://app.etherpk.com/demo)
I wish Office 365 would hire someone like the author. Online Word never renders the same as it would in the Desktop software...and this matters. I deal with only modestly complex documents when I write grants but I do need to stay within page length limits for sections, I need some layout stability...so I inevitably fire up my Windows VM to run the application.
Before anyone suggests LateX, I know, I know, but when you write collaboratively, you don't always get to choose the platform.
Was actually going to suggest Typst.
Microsoft had a blog post somewhere talking about how they're using desktop rendering code via wasm and canvas to get to parity.
Don't know how complete that effort is though. Haven't checked myself.
Google has switched in 2020 (if i am not mistaken to canvas), but I am not saying this would be the solution to everything, It is just another option one could do. I do not know if someone noticed this, but that is part of the reason you could not just open google docs on mobile. Because I would suspected that it would been too much effort for them back than. In anyway Tasfer tried as best as possible to do that on mobile too.
I wrote a similar tool to this many years ago that used JATS as an internal document model, using contentEditable, and I can relate to the pain you refer to. Getting the semantics right in cases where you split or join text that has varying types of nested context was hard.
I like what you did here in terms of the cursor that points one way or the other: it tells you if you're in "normal" mode, or for example, in a bold region. I don't think I've seen that before so, if you made it up, that itself is a cool invention.
That's a really nice invention. In general I much enjoy the editor, it's refreshing how it operates, it has many good ideas. But it's not working as it should? How to delete the bold markup, for example? Can't get it done without using the Ctrl+B shortcut, whereas I could create it by typing markdown. When I delete the first bullet, undo doesn't restore it. When i delete the second, undo jumped the cursor to the first bullet.. So there's work to be done still. When copying the text, I'd like to get the markdown text, not some sterilized version of it (leave that to Shift+Ctrl+V if it's needed).
Just tested it on Android, glide delete and glide cursor control doesn't work.
Would it even be possible to make those work with a canvas?
WOW, I have never know that these gesture even existed. I am not big mobile user. But i will diffently look in thoses.
And that’s a part of why I said the approach is bad: there’s a long tail of native features that depend on being able to see/interact with more than just the current word or selection or whatever, and you’re thwarting that. You cannot make a lot of them work, and the more effort you put into trying to cover them, the more you end up with something even unholier than contenteditable.
Give up. Seriously, please. Switch to DOM, it is genuinely mandatory for acceptable results.
You can get close enough on desktop that many users won’t notice, but you emphatically cannot reach even tolerable on mobile.
We should chat (check bio)! For almost a decade now I’ve been working on a writing tool that might benefit from it. I build occasional forks with procgen on top it. That being said better bidi handling is something I’m most interested in.
Enso.sonnet.io - what I’m talking about.
(On untested.sonnet.io you’ll find notes about the experiments / toys related)
Nice work, liked the idea. Come join Tasfer Discord, there is one there. I just added the link to it in landing page.
Ok the custom rich text latex editor is really cool. But I'm on a tablet and stuff like selecting text is a bit awkward (the up down auto scroll is not exactly the same as the native one and isn't as precise as the native one).
But still this is an awesome project!
how are you handling IME and screen readers with canvas? also do you relayout per line or per block? only relaying out changed blocks was the biggest typing speedup in my markdown editor
there is a hidden "<div contenteditable=true>" which contains the entire line being edited, which causes IME to work
Yes, exactly that is the input surface. Canvas can not recieve the events otherwise, IME, clipboard and others events go thraugh that.
The editor swallows all accented characters (ex áã, ö, è).
not for me. might want to specify your browser/os.
If you have time, really interested on where you faced this issue, anything would help.
Same thing was done in Flash back in the day :)
What UI library did you use? Looks clean.
I used radix UI, but it has no styling.
Cool idea! I approve of the privacy oriented design. But it looks like you didn’t actually test this on iOS as automatic text correction is completely broken.
Also your website appears to have been written and generated entirely with an LLM. That doesn’t inspire trust.
There’s no better way to get a lot of testers than to have a Show HN.
I’ll confirm - there are cursor bugs on iOS. Both on newlines and autocorrect.
I’m personally more forgiving of an LLM “helped” site, if it looks like there was a human in the loop for the process. An LLM has more context about the design goals and architecture than the reader, so if it’s an informative page, I’m not personally bothered.
But I’m probably in the minority.
A human is needed to show how app’s technical features are relevant to a particular audience with a shared social context. As the LLM is not (and should not ever be) a participant in that social environment, any marketing it generates will almost certainly land as tone deaf or presumptuous.
That’s the “human in the loop” part. Use the LLM to generate a copy and the human to refine it.
If you think successful campaigns these days are always entirely done by humans, then I’m afraid you’re mistaken.
I take advice and feedback like yours seriously, thank you for giving your opinion, I appreciate it. For iOS, I tried to make it work, but iOS has really made it tough in that regard. For some reason they do not like a hidden contenteditable controlled by a canvas. That is part of the reason I have an iOS app published on the App Store.
Also, I know that LLMs might sound wrong to many, and I get where this is coming from.
For me, AI gives me, as one lone developer, the power to create and implement these ideas. It helped me translate and do so much more. I agree this may create a feeling of distrust, and I don't think it should not replace thinking.
iOS was telling you “don’t do this, it won’t work”, and your answer was to ignore it, ship something broken, and make an app for certain platforms as well.
The LLM will happily help you to implement the wrong thing. It’s not good at saying “you can try doing this, but the result will never be good”.
This is a good demonstration of why you really shouldn’t do this. Bluntly: this is completely unusable for many people, barely usable for a lot more, and on desktop and mobile both I would absolutely hate to use this, and a lot of the problems in it fundamentally cannot be fixed.
> But it also means the editor looks and behaves the same in every browser, on desktop and on phone, because nobody else is in charge of the pixels.
You say this as though it were a good thing. It’s very much not; they’re not supposed to be the same, they have slightly different expected renderings, and significantly different interactions.
And you can’t inspect or emulate them.
—⁂—
First of all, Firefox under Linux. I see it renders with my chosen fonts, indicating (since you’ve said you’re using 2D canvas) that you’re using fillText. Good; I would complain more about the alternative. But it does kinda go against a lot of what you said about who was in charge of the pixels.
I use my Compose key. Underlined · placeholders, proper replacement, &c. Huh, you’ve surprised me by actually handling that correctly. Well done.
Though I do note that when I type U+1F928 FACE WITH ONE EYEBROW RAISED in a title, in the app’s sidebar and top-of-page title it becomes U+1F929 GRINNING FACE WITH STAR EYES. That’s… not encouraging, but ultimately presumably Just a Bug.
I switch to Telugu and type “telugu”, తెలుగు. Uh oh… with each new consonant it drops the completed syllable, and when I press Escape at any point it deletes the composition. Utterly debilitating bug for IME usage, but ultimately fixable.
I enter some text, find that for the most part it works significantly better than I expected, even handling things like clipboards pretty well.
Then I turn to what I know it will fail, because it’s not possible to get correct universally (different platforms have different behaviours and you can’t probe it; user-agent/platform sniffing would get the best results): keyboard navigation. Ugh, Ctrl+{Left,Right} is utterly wrong. I expected it to have implemented perhaps Windows’ behaviour and I’d complain about that, but no, it’s stopping at both the end of one word and the start of the next, which no platform has ever done. Ctrl+{Up,Down} doesn’t work by paragraphs but still by line. PageUp/PageDown kinda work but the amounts they move by are just wrong, very wrong. And so on. Cursors are frustratingly non-native the while (becomes move when hovering over selected text).
I check whether Backspace seems sane on Telugu text. Seems to be, to my satisfaction. Ctrl+Backspace, of course, is utterly wrong like Ctrl+Left was.
I decide to skip testing any BiDi. But I do run through some of https://lord.io/text-editing-hates-you-too/, and watch it fail vertical cursor movement, affinity, emoji modifiers… meh, BiDi is here, so I decide to test it once and oh dear while it supports the general concepts properly (including selection) it seems to think that if you give it a little bit of RtL in an LtR paragraph it should turn the paragraph RtL which is badly wrong, and then it handles other aspects of selection and keyboard interaction badly wrongly. And it already failed composition. So yeah… it failed them all, where Firefox handles them all correctly.
I try zooming with my touchpad. Nothing happens if the cursor is over the canvas. Grr. Works but leaves the canvas blurry if the cursor is outside it. (Fixable.)
I paste the full text of Winnie-the-Pooh <https://www.gutenberg.org/cache/epub/67098/pg67098-images.ht...>. It locks up the browser tab for several seconds. (Not fundamental, but not surprising either. The browser can handle pasting a mere 26,000 words with light markup in an instant.) And takes perhaps minutes more in the background to decide it is failing to upload all the images (and regularly goes and tries to load them again later). It also inserts blank paragraphs between every paragraph and turns italics into underscore wrapping for no apparent reason. And mangles “*** START OF THE PROJECT GUTENBERG EBOOK WINNIE-THE-POOH ***” into italic “\\\\\\ START OF … \\\\\*”. Not encouraging, though once again not particularly about canvas text editing.
Now the document is long enough, I try scrolling. </i>Oh no.* Scrolljacking!? That was completely unforced error! You must use a real scrolling area. Scrolling is now utterly tedious for me, because it’s not possible to implement scrolling yourself from the events exposed to you, and whatever monstrosity it’s doing instead gets the scroll rate wildly wrong and lacks inertia, so a full swipe on the touchpad gets almost a pageful instead of several plus a hundred more if done fast. Combined with it hiding its fake scrollbar until you scroll, large documents are nigh unusable. Now once again, this is not fundamental to canvas text editing, so long as you’re willing to put the thing in a proper scroll area that you maintain.
I switch to Firefox for Android with the FUTO keyboard. Initially things seem to be working well, better than I expected, particularly given how I’ve observed the backing text input is generally only being given a word at a time—I think FUTO must be remembering words itself for its suggestions, working around how badly broken what it’s being given is. But then I try to shift the cursor by sliding my finger along the Space key, and it doesn’t work. I switch to something else and come back, and selection is shot, it’s lost the caret and I don’t know what happened to it, and various keyboard interactions are broken. I press and hold on a word to select it as always works, and… it doesn’t: no, it seems like they’ve implemented something a little more like my vague memories of iOS from when I’ve seen that; and badly at that. Correct selection is rendered tedious, unreliable, and sometimes impossible. And as for doing anything with that selection… it offers Copy, Cut, Paste. Utterly inadequate, stopping the browser from doing stuff with the text (e.g. share to another app), but unavoidable in the canvas approach.
At this point I give up; it’s completely unusable for anything but the basics. A lot of what I complained about is just that this is buggy and incomplete, but a lot of it is also fundamentally unfixable.
Even the buggy and incomplete parts indicate why the approach is, to put it gently, risky. But the unfixable parts—
This is why you should only use contenteditable.
And I didn’t even get to throttling my CPU to 400MHz to test performance. But it’s after half past midnight and I should be asleep already.
This is really valuable! Just to add a bit of context: I'm not the only one using canvas for rendering. Google Docs did it years before me. Think of it this way: I've built this almost entirely alone and still get most of the basics working.
Let's start with your test. I think the reason it struggled with the Gutenberg text is that it had a lot of images. Either way, that's a bug and it should be fixed (it shouldn't load all the images at once, and there's probably too much work happening on the main thread). I really appreciate the effort you put in.
I could also use a WebGPU or WebGL context, which would certainly be faster. I think I had a working version last year, but it had problems with blurry rendering (font hinting). I don't remember the exact details now.
Overall, I'm really thankful that you pointed these out. I'll try to work through them over the next few days. I do disagree with the premise, though: canvas is more than capable of achieving this. Performance shouldn't be an issue. That said, I agree with your points about integration with context menus and other tooling.
I have pointed out various fixable bugs, but I also pointed out some critical things that are fundamentally unfixable, and make the entire approach unbearable. You cannot make it acceptable on a pretty large fraction of mobile devices, and even where it’s not horribly broken, it’s still radically different from every single other thing on the device in extraordinarily frustrating ways.
As for Google Docs, well, I’ll just link to what I’ve written about it before: https://news.ycombinator.com/item?id=42253177. I honestly believe they lied in their justification, because the resulting experience is genuinely worse in every way. I can’t comment on how it is on mobile, because I’m too lazy to log in, and they probably do something different on mobile anyway.
What choice do we have? We have to find a solution to the problem. ContentEditable is much harder to work with. This is a solution, and I hope we will at some point work out a new web API that makes these tasks easier.
Your reasoning is that I am missing out on a large percentage of users. I understand where this is coming from. I am working on mobile apps as well, as I did with iOS. I can try to close that gap even more, but I can't just switch to ContentEditable. I don't have the time, and there are already tons of projects that do that.
But if Google did it at their scale, then I think the tradeoff is reasonable, don't you think? You said in another HN post that they don't really use pure canvas. What do you mean? ContentEditable for capturing input?
By the time you’ve fixed as much as you can, you will certainly have more code, more complex code and more fragile code than you would have had from massaging contenteditable into your desired shape, and your result won’t even be better than it in any indisputable way.
“Pure canvas” means when the entire app runs inside a canvas to the maximum extent possible, such as with https://www.egui.rs/. Google Docs isn’t pure canvas because it uses regular DOM for the chrome, leaves text rendering to the browser (fillText or whatever) and uses real scrolling area (multiple canvases, tiling). Yours is similarly not pure canvas, though it’s a little closer in the obviously-bad way of how you implement scrolling yourself.
“Google did it” is not an endorsement for others doing it. They have often quite bizarre rationales that truly don’t apply to smaller organisations. As I have written, I’ve thought it through carefully multiple times in multiple ways, and I honestly believe they were lying about their motives through and through, because they just don’t make any sense. Google Docs was unambiguously better before they migrated to canvas, and most of their claims about what it got them were demonstrably false.
> Though I do note that when I type U+1F928 FACE WITH ONE EYEBROW RAISED in a title, in the app’s sidebar and top-of-page title it becomes U+1F929 GRINNING FACE WITH STAR EYES. That’s… not encouraging, but ultimately presumably Just a Bug.
Fun discovery: this is a Firefox bug. Address bar, text areas, &c. and it’s consistently rendering the wrong character, but somehow your canvas got it mysteriously right. Wild! I should update my Nightly (couple of weeks out of date) and see if it’s still happening, and file a bug report if it is.
This looks slick but I think you need a better way to market it.
The 2D canvas part is the norm for anyone who wants performance. Working with the DOM becomes painfully slow and resource intensive the moment you add syntax highlighting, spell checking, and other visual chrome, to large documents.
This is why Monaco (the editor widget behind VSCode) is a canvas. Why terminal emulators built with web technologies use canvas. And so on.
So I wouldn’t say the canvas part of the heading is a differentiator.
Edit: apologies, seems I was wildly wrong here. Please disregard the above.
> The 2D canvas part is the norm for anyone who wants performance. Working with the DOM becomes painfully slow and resource intensive the moment you add syntax highlighting, spell checking, and other visual chrome, to large documents.
This is wildly wrong. 2D canvas is consistently slower than letting the browser handle the rendering, so long as you avoid making the DOM too complex, and possibly monitor the viewport and only keep some of the content in the DOM at a time (though for most applications this practice is somewhere between harmful and not useful, and it definitely adds a lot of complexity).
> This is why Monaco (the editor widget behind VSCode) is a canvas.
It isn’t.
> Why terminal emulators built with web technologies use canvas.
Why on earth would you build with web technology, if you’re then going to use canvas? I can’t immediately think of any that match your description (though I have no doubt that in-browser ones using canvas exist).
> This is wildly wrong
You’re right. I’ve been experimenting with Monaco lots lately and could have sworn it used a canvas. But it doesn’t.
> 2D canvas is consistently slower than letting the browser handle the rendering, so long as you avoid making the DOM too complex, and possibly monitor the viewport and only keep some of the content in the DOM at a time (though for most applications it’s somewhere between harmful and not useful, and it definitely adds a lot of complexity).
Interesting. I’ve found the reverse to be true. But it sounds like this is human error on my part. (probably doesn’t help that I’ve had more experience building high performance GPU applications in Dx, SDL, and similar than I have building high performance web apps.
Thanks for the correction.
> Why on earth would you build with web technology, if you’re then going to use canvas? I can’t immediately think of any that match your description (though I have no doubt that in-browser ones using canvas exist).
You answered your own question.
And I do know of at least one written using the canvas. Because I wrote it. I’m now interested in how others work though.
Most in-browser terminal emulators just use plain old boring DOM, and ignore the issue of perfect columns. Some detect it and wrap offending characters in fixed-width spans. But the entire concept of columns is unsound, doesn’t work at all for a lot of scripts.
It might seem unsound but it’s the concept we’ve built our terminals around.
And there are already definitions and APIs to decide the terminal cell width of different Unicode characters.
I’m not saying it’s easy, because you end up with 3 different string widths:
1. memory length in bytes
2. Number of Unicode characters (which is annoyingly also different to the number of Unicode code points since some codes can be combined)
3. terminal cell width
But as annoying as it is, there are a few relatively mature libraries you can use to solve this problem.
> And there are already definitions and APIs to decide the terminal cell width of different Unicode characters.
And how! I know of four different and mutually-incompatible definitions/behaviours, and I’m confident there will be more; and once you get to things like Indic scripts and Right-to-Left there are no good answers, and no cell-based terminal produces even vaguely acceptable results.
As for meanings of string width, I’d count at least four: Unicode code units, Unicode scalar values, Unicode extended grapheme clusters, and terminal cells.
> And how! I know of four different and mutually-incompatible definitions/behaviours, and I’m confident there will be more;
I’ve not found that to be the case. Usually the issue is more around different libraries being out of date (ie pinned to an earlier version of Unicode)
> and once you get to things like Indic scripts and Right-to-Left there are no good answers, and no cell-based terminal produces even vaguely acceptable results.
That’s because right-to-left isn’t supported and in my opinion shouldn’t be supported my the terminal emulator itself.
Right-to-left support would be handled by the client application, not the terminal emulator. The terminal is just there to offer a grid of Unicode cells. It’s not a writing composition suite.
Just like how the terminal emulator isn’t readline, it isn’t emacs, it isn’t hexdump, and it isn’t vim.
> As for meanings of string width, I’d count at least four: Unicode code units, Unicode scalar values, Unicode extended grapheme clusters, and terminal cells.
I’d already touched on the fact that Unicode widths can vary depending on what you’re measuring. But your Unicode library would/should handle most of the pain there.
When writing software for a terminal, you’re really only interested in the characters you need to loop through, their cell width, and, in some languages, the number of bytes.
As I’ve said previously, I’ve written terminal emulators targeting a variety of different renderers like SDL (so not just js+canvas), as well as complex terminal software like $SHELLs. From what I’ve built over the decades, honestly Unicode support isnt as big problem as it used to be. Sure, there’s plenty of footguns that you need to be aware of, but that’s just the nature of working with technology originally designed in the 60s/70s. Like how you need to be aware of cell widths if you intend to do any manual text alignment in client software. But at least there are libraries to make that task easier.
And if you don’t case about text alignment, then the terminal emulator will handle that for you.
There are several quite different things called wcwidth, in part because as a POSIX function it wasn’t well-specified; now I think there are two main families of it, maybe still three; https://wcwidth.readthedocs.io/en/latest/specs.html is the best-specified and -considered of them, but not the most popular. Then there are others that see how wide a character renders, and quantise in some fashion to cell. (This obviously requires knowledge of the font, which should be unavailable to the shell, so it’s a technique only terminals can use.) And in all of these, there are multiple incompatible versions hanging around. The aforelinked one shows some of its changes over time as they wrestle with trying to get things as reasonable as possible, which is fairly obviously hopeless anyway. (Some characters are just too wide, some extremely so, some less so. U+FDFD is generally made 1×1, but it should truly be something like 2×2 or 1×8 or a little wider depending on whether the font employs https://en.wikipedia.org/wiki/Word_heaping. Or the widest Telugu consonant, y, which wants to be 2 columns by itself or 3 or 4 columns with some conjuncts and vowels (e.g. ykuu, య్కూ), but gets squished to one by itself or two in those wider cases.)
The terminal emulator must handle a lot of RtL stuff to get it acceptable. In raw mode (where dimensions are knowable) you could almost leave it to the app, but outside raw mode it’s pretty obvious that the terminal has to be BiDi-aware. But even in raw mode, you really can’t treat it as you describe, because Arabic text rendering is rather complex, full of necessary ligatures.
Unicode defines the width of characters. This is what the POSIX functions like wcwidth refer to. eg https://www.unicode.org/reports/tr11/
So you shouldn't need knowledge of the type-face to know the size of the printed character. If other libraries are printing the characters and then extracting the width from that, then they're doing it wrong and you should look to use a different library instead.
The bigger problem I've run into is not different libraries making up their own width assumptions. It's been libraries built against outdated versions of the Unicode standard.
> The terminal emulator must handle a lot of RtL stuff to get it acceptable. In raw mode (where dimensions are knowable) you could almost leave it to the app, but outside raw mode it’s pretty obvious that the terminal has to be BiDi-aware. But even in raw mode, you really can’t treat it as you describe, because Arabic text rendering is rather complex, full of necessary ligatures.
I think RTL is a distraction from the actual point that we're discussing. But it's worth noting that there are already CSI sequences for enabling RTL behaviour in terminal emulators. It's not something I've personally implemented in my own projects because I have no personal need for it and have had no feature requests from people asking for it who could test the implementation's soundness (unlike Chinese characters, which have seen a reasonable degree of popularity in the projects I've built). But I'm sure some other terminals support it just fine since it was added decades ago as part of ECMA 48.
No, UAX #11 does not define the width of characters. It’s called “East Asian Width” for a reason. As the note in the Scope section begins:
> The East_Asian_Width property is not intended for use by modern terminal emulators without appropriate tailoring on a case-by-case basis. […]
It’s about a very specific typographical custom in a specific set of languages. Terminal emulators have piggy-backed onto it because it’s fairly close to what they want and definitely better than nothing, but if they limit themselves to this definition, they do a bad job.
> you shouldn't need knowledge of the type-face to know the size of the printed character
To see the most extreme example, look through Google Fonts for U+FDFD <https://fonts.google.com/?preview.text=%EF%B7%BD> and see the radical differences in aspect ratio (and size!). In some other scripts there are multiple ways of writing things which have quite different width characteristics. In English we have different accepted and understood letterforms for a, g, v, z and probably others, but they’re all roughly the same width. But this isn’t always true in other scripts. My name is written in Telugu as క్రిస్ (kri s), but that r curve can be drawn below or to the left (both seen among <https://fonts.google.com/?preview.text=%E0%B0%95%E0%B1%8D%E0...>), and even sometimes a little to the right; and you don’t actually have to write it with a conjunct, క్ రిస్ (k ri s) means the same thing and there used to be some fonts or apps that wouldn’t render conjuncts. (Encodingwise, I inserted a ZERO WIDTH NON-JOINER after the {k, suppress vowel} and before the {r, -i} to simulate this effect. And then HN turned the ZWNJ into a space. Sigh. I should ask them once again to stop doing that.) Of these three different legitimate renderings of the same text, one works in two cells, one wants two and a half, one needs three.
> No, UAX #11 does not define the width of characters.
It's literally there in the doc
> It’s called “East Asian Width” for a reason. As the note in the Scope section begins:
Yes. I didn't claim it defines terminal cell widths. There's a reason you need a library to translate that into cell widths. What I said was you don't need to be pratt about with printing because Unicode does define widths and properly libraries pin against that (like POSIX does).
> Terminal emulators have piggy-backed onto it because it’s fairly close to what they want and definitely better than nothing,
That's a very convoluted way of agreeing with me :)
> but if they limit themselves to this definition, they do a bad job.
A large part of the problem here is you're introducing scope creep.
> To see the most extreme example, look through Google Fonts for U+FDFD <https://fonts.google.com/?preview.text=%EF%B7%BD> and see the radical differences in aspect ratio (and size!).
You're shifting goal posts again because you're no longer talking about fixed width typefaces. Terminals have to make some concessions for turning scripts (of any language) into fixed width. If that's an issue then you shouldn't be using the terminal to begin with. There are better standards for that, such as the web.
In short, nobody is going to disagree with you that Unicode is hard and full of edge cases. But the vast majority of the stuff you describe are either already solved, already have standards, or are already out of scope for terminals.
As far as I know, Monaco, the editor behind VS Code, does not use canvas. I noticed that Google Docs uses canvas, and that was the start of my project a year ago. I think that if I had used another context, like WebGL or WebGPU, it would have been much more performant, but it is really hard to render text and parse font files that way.
Anyway, I have gotten good enough results, and I don’t think normal usage will exceed the limits of the 2D canvas.