fjvallarino/monomer

I finally found some time to revisit this.

Closed this issue · 18 comments

(I could find how to reopen the issue 83#)

I finally found some time to revisit this.

grafik

I was able to add the following functionallities:

  • Syntaxhighlighting
  • Linenumbers && highlighted current linenumber
  • Highlight current Line
  • Selection works as expected
    all handled by config options for textArea.

Two things though:

  • It could not fix the jumping of the linenumbers - I tried to remove the padding as you suggested, but it still jumps back and forth
  • My syntaxhighlighting method looks okay, but if you switch it on an off ... there is movement in the text, so I make mistakes in my alignment of the text

Maybe you have a tip on how to deal with these two things

This is the code for the renderer in TextAreaWidget (I'm using a open repo for this with only the nesseary code - whole monomer took quite long to build everytime):

  render wenv node renderer = do
    let font = fromMaybe def (style ^? L.text . _Just  . L.font . _Just)
    let fontSize = fromMaybe def (style ^? L.text . _Just . L.fontSize . _Just)

    -- Highlight current line
    let textLinesRect   = wenv ^. L.viewport & L.x +~ lineNumberWidth
    drawInScissor renderer True textLinesRect $
      drawInTranslation renderer offset $ do
      case _tacCurrentLineColor config of
        Nothing -> return ()
        color   -> do
          let indexCurrentLine   = snd $ _tasCursorPos state
          let currentLine        = Seq.index textLines indexCurrentLine
          let (Rect tx ty tw th) = _tlRect currentLine
          let maxLineLength      = Seq.foldlWithIndex (\startValue _ textLine -> do
                                                          let tmpLength = _rW (_tlRect textLine)
                                                          if tmpLength >= startValue
                                                            then tmpLength
                                                            else startValue) 0 textLines
          drawRect renderer (Rect (-10) (ty-5) (10+maxLineLength) (th+10)) color Nothing

      when selRequired $
        forM_ selRects $ \rect ->
          when (rect ^. L.w > 0) $
            drawRect renderer rect (Just selColor) Nothing
        

      -- Syntax Highlighting
      case _tacSyntax config of
        -- Apply syntax highlighting
        Just (Just syntaxTree,syntaxMap) -> do
          let textLines' = Seq.mapWithIndex (\index value -> value & L.fontSize .~ fontSize) textLines
          MyDrawing.drawTextSyntaxed renderer style syntaxTree syntaxMap textLines'
        -- No syntax highlighting
        _          -> do
          let textLines' = Seq.mapWithIndex (\index value -> value & L.fontSize .~ fontSize) textLines
          forM_ textLines' (drawTextLine renderer style)

      when caretRequired $
        drawRect renderer caretRect (Just caretColor) Nothing


    -- draw LineNumbers
    let lineNumbersRect = wenv ^. L.viewport & L.w .~ lineNumberWidth
    let otherStyle color = style & L.text . non def . L.fontColor ?~ color
    let rH' = if textLines ==  Seq.empty
              then 5
              else do
          let line   = Seq.index textLines 0
          let space  = unFontSpace $ _tlFontSpaceV line
          let height = _rH $ _tlRect line
          height + space
    let textLine t y = do
          let wvp = fromMaybe (wenv ^. L.viewport) (removeOuterBounds style (wenv ^. L.viewport))
          let rect' = wvp & L.x +~ 5 & L.y .~ y & L.w .~ 20 & L.h .~ 20
          --let rect' = Rect (wenv ^. L.viewport ^. L.x + 5) y 20 20
          def
            & L.text .~ t
            & L.rect .~ rect'
            & L.fontSize .~ fontSize
            & L.font .~ font
    let renderLineNumber = renderIndexLine . createIndexLine
          where
            lineStyle index =
              otherStyle $ if index==succ (snd $ _tasCursorPos state)
                           then rgbHex "#ff2200"
                           else rgbHex "#000000"
            renderIndexLine (index,line) = drawTextLine renderer (lineStyle index) line
            createIndexLine x            = (x,textLine (T.pack $ show x) (35 + fromIntegral x * rH'))
    when (fromMaybe False (_tacShowLineNumbers config)) $ do
        drawRect renderer lineNumbersRect (Just (fromMaybe (rgbHex "#ff2200") (style ^. L.sndColor))) Nothing
        drawInScissor renderer True lineNumbersRect $ do
          mapM_ renderLineNumber [1..length textLines]
    where
      style = currentStyle wenv node
      contentArea = getContentArea node style
      ts = _weTimestamp wenv
      offset = Point (lineNumberWidth + contentArea ^. L.x) (contentArea ^. L.y)
      focused = isNodeFocused wenv node

      caretTs = ts - _tasFocusStart state
      caretRequired = focused && even (caretTs `div` caretMs)
      caretColor = styleFontColor style
      caretRect = getCaretRect config state False

      selRequired = isJust (_tasSelStart state)
      selColor = styleHlColor style
      selRects = getSelectionRects state contentArea

Originally posted by @simmihugs in #83 (comment)

Hi! The issue of the line numbers' jittering is most likely caused by rounding. After testing for a while, I think the most reliable solution is to use drawInTranslation instead of manually adding the offset. The resulting code would be:

    ...
    let textLine t y = do
          let wvp = fromMaybe (wenv ^. L.viewport) (removeOuterBounds style (wenv ^. L.viewport))
          -- Do not use viewport's x here; just set the desired padding values
          let rect' = def & L.x .~ 5 & L.y .~ y & L.w .~ 20 & L.h .~ 20
          def
            & L.text .~ t
            & L.rect .~ rect'
            & L.fontSize .~ fontSize
            & L.font .~ font
    let renderLineNumber = renderIndexLine . createIndexLine
          where
            lineStyle index =
              otherStyle $ if index==succ (snd $ _tasCursorPos state)
                           then rgbHex "#ff2200"
                           else rgbHex "#000000"
            renderIndexLine (index,line) = drawTextLine renderer (lineStyle index) line
            createIndexLine x            = (x,textLine (T.pack $ show x) (35 + fromIntegral x * rH'))
    when (fromMaybe False (_tacShowLineNumbers config)) $ do
        drawRect renderer lineNumbersRect (Just (fromMaybe (rgbHex "#ff2200") (style ^. L.sndColor))) Nothing
        drawInScissor renderer True lineNumbersRect $ do
          -- Apply x offset here
          drawInTranslation renderer (Point (wvp ^. L.x) 0) $
            mapM_ renderLineNumber [1..length textLines]
    ...

I'm not sure I understand the syntax highlighting issue; I tried switching it back and forth and I have not noticed anything strange. Let me know if it still happens how I can reproduce it (or what to look for), and I'll take a look.

Regarding using a separate repository, that's definitely the best solution. You will reduce the compile times drastically. On the other hand, I suggest you use ghcid for development. If you modify the .ghcid file to have this content to (I only updated the name of the library):

--command "stack repl --main-is MyTextarea:exe:app"
--test ":main"
--restart=package.yaml

and run ghcid from the console (you can install it from the project's root with stack install ghcid), you will be able to make changes and test them much more quickly. There is more information here.

Your code editing widget is looking great! This widget is so much more than textArea and deserves its own name/package. If you happen to publish it publicly, let me know and I'll link to it from Monomer's home page.

Hi @fjvallarino,

  1. Linenumber jumping
    I updated the repo to use your code you posted above - And It works now which is great
  2. ghcid
    I followed your recommondation and updated the ghcid file - the only reason why I'm not using ghcid all the time is because
    I found the performance better with stack build and stack exec app - as it should be I guess because it's compiled (?)
  3. syntax highlighting
    I created a gif - for example look a line 14 the ( and the Constructor behind are effected differently by using or not using
    the syntax highlighting. When using syntaxhighligthing I split each textline based on the syntax rules and draw each element
    using the color from the syntaxrule and the _glpXMin for the relating glyph. That seems to be correct for things like the
    Constructors but not for the Symbols.
    syntaxhighlighting
    Generally speaking making use of Loc and Token from GHC-SyntaxHighlighter is propably okayish for haskell code but
    to make it more applicable for different languages I should change it to use some more generic data type I guess. That
    would also allow for other tokens - if other languages have different tokens.
  4. About the Widget generally speaking
    I would be fine if you are interessted to include this widget in monomer directly. But of course publishing it separate from
    monomer an linking it from monomer would also be an option of course. Before any of that happens I want to figure out
    how to avoid the jumps in the syntax and a proper way to deal with syntax for different languages.

I've been looking into this, but I have not found a solution so far. The problem seems to arise from differences in DPI handling between NanoVG and FontManager (which is extracted from NanoVG but returns glyphs already scaled with the current DPI). In the end, it's another issue related to rounding. For example, if in your MyDrawing.hs file you change:

let txtOrigin = Point (tx+x2) by

to:

let txtOrigin = Point (fromIntegral (floor (tx + x2))) by

you will notice some improvements, although the (%~) in line 7 still jumps back and forth. As you expect, rendering individual glyphs or the full text-line should give the same result, and this is something the library should provide (hence, it's an issue on the library's side and not your code).

It's going to take me some time to get back to this, since I have a few other things scheduled to work on in the library. I'll let you know when I make some progress.

In the end, it was an obvious error in the initialization of Monomer's FontManager 🤦🏻‍♂️.

I created a PR that I will keep around for a couple of days, since I want to validate nothing looks weird.

To use it, you need to update your stack.yaml to use this commit hash:

- git: https://github.com/fjvallarino/monomer
  commit: 46f22c2d621e644a573d10cfaaae10ebcec5ed0d

You will also need to make a few updates since Timestamp is no longer an alias to Int, but its own newtype. The timestamp related definitions should now look like this:

...
defCaretMs :: Timestamp
...
_tacCaretMs :: Maybe Timestamp,
...
instance CmbCaretMs (TextAreaCfg s e) Timestamp where
...
_tasFocusStart :: Timestamp
...

The findWidgetIdFromPath function has been deprecated (not yet removed), so it's a good idea if you replace its usage by:

scWid = widgetIdFromPath wenv scPath

I applied your recommended changes - also pushed to the mentioned repo
But I still observe the jumping with switching on and of on the syntaxhighlighting.

That is weird. I just pulled the latest version from your repository, rebuilt it from scratch, and it works fine. Have you tried running stack purge, then stack build?

I changed the stack.yaml and when stack build

but I gonna check with stack purge right away

I run into an issue with stack purge - on windows I get an exception which tells me I dont have permission to delete .stack-work this still happens when I run it with an admin terminal.

I tried to delete .stack-work by hand and rebuild - but it did only rebuild my code in the repo

Then I did some googling and used stack exec -- ghc-pkg unregister monomer, delted the .stack-work again and run stack build

I did not get a build error - and as timestamp is defined in the release it supposedly used the correct version of monomer
but I still get the jumps

I tested disabling hdpi (this is not a flag exposed by Monomer's configuration, it's part of SDL initialization) and could reproduce the issue again (it didn't happen with hdpi on). It seems the difference is in a scaling factor nanovg applies to all its text-related operations. I could not find an easy way to reproduce that formula since it relies on internal state, but scaling by a largeish factor appears to work.

You will need to update your stack.yaml again:

- git: https://github.com/fjvallarino/monomer
  commit: a7f9fd904f5a9094733d5f209c79d8acef029916

Please let me know if this improves the situation for you. Thanks!

Changed to the new branch - now it works great!!!

linenumberjumping fixed (really)

Now I found an issue regarding the linenumbers - I have to make the linenumbers start in relation to the textArea widget not hard code like I do it right now, so still some stuff to do before I can publish it but again thanks alot for your assistance with the syntax higlighting
grafik

You need to consider the vertical position of your codeEditor. Since you are, for the line numbers section, trying to override the offset scroll sets, you will also need to account for the y coordinate (you already do for x). I tested now and this seems ok:

    let renderLineNumber = renderIndexLine . createIndexLine
          where
            lineStyle index =
              otherStyle $ if index==succ (snd $ _tasCursorPos state)
                           then rgbHex "#ff2200"
                           else rgbHex "#000000"
            renderIndexLine (index,line) = drawTextLine renderer (lineStyle index) line
            -- Modified (text starts at 1, but for calculations they run from 0 to n -1)
            createIndexLine x            = (x,textLine (T.pack $ show x) (fromIntegral (x - 1) * rH'))
    when (fromMaybe False (_tacShowLineNumbers config)) $ do
        drawRect renderer lineNumbersRect (Just (fromMaybe (rgbHex "#ff2200") (style ^. L.sndColor))) Nothing
        drawInScissor renderer True lineNumbersRect $ do
          -- Apply x offset here
          -- Modified. Also apply y offset here. It uses the node's viewport (without subtracting padding, as contentArea does)
          drawInTranslation renderer (Point (wvp ^. L.x) (node ^. L.info . L.viewport . L.y)) $
            mapM_ renderLineNumber [1..length textLines]

I applied your changes, only I in the meantime added config options for colors for linenumberbackground, linenumber, current linenumber. But I think your solution is slightly of to the top so I added 3 + node ^. L.info . L.viewport . L.y. So there might be some padding missing?

    let renderLineNumber = renderIndexLine . createIndexLine
          where
            lineStyle index =
              otherStyle $ if index==succ (snd $ _tasCursorPos state)
                           then fromMaybe (rgbHex "#ff2200") (_tacLineNumberNumberHighlightColor config)
                           else fromMaybe (rgbHex "#000000") (_tacLineNumberNumberColor config)
            renderIndexLine (index,line) = drawTextLine renderer (lineStyle index) line
            -- createIndexLine x            = (x,textLine (T.pack $ show x) verticalPosition)
            --   where
            --     verticalPosition = contentArea ^. L.y - 2 - 25 + fromIntegral x * rH'
            createIndexLine x = (x,textLine (T.pack $ show x) (fromIntegral (x - 1) * rH'))    
        
    when (fromMaybe False (_tacShowLineNumbers config)) $ do
        drawRect renderer lineNumbersRect
          (Just (fromMaybe (rgbHex "#11ff00") (_tacLineNumberBackgroundColor config))) Nothing
        drawInScissor renderer True lineNumbersRect $ do
          -- Apply x offset here
          -- Modified. Also apply y offset here. It uses the node's viewport
          -- (without subtracting padding, as contentArea does)
          --drawInTranslation renderer (Point (wvp ^. L.x) 0) $
          drawInTranslation renderer (Point (wvp ^. L.x) (3 + node ^. L.info . L.viewport . L.y)) $
            mapM_ renderLineNumber [1..length textLines]

I changed the name of the https://github.com/simmihugs/codeEditor to codeEditor and added a library to the package. I allready tried to use the library in another project which works.

That's strange: it looks off if I add the +3 you mention. In my case, the numbers look aligned with the text, although not with respect to the highlight box.

I allready tried to use the library in another project which works.

Nice!

The PR for the glyph positioning issue has been merged to main, making the previous commit hash invalid.

You can use this in stack.yaml until 1.4 is released:

- git: https://github.com/fjvallarino/monomer
  commit: b80e2ca0360b5d740c9e1e9e8a07e00457744f6b

Once you update, the build of your component will fail because of a change where the Timestamp type was renamed to Millisecond. If you search and replace in your code, it should compile fine.

I found out that the offset is nessecary when a larger font size is used (compare screenshots below)
Next I will update to the new release

What I get - on the left with the offset* on the right without
That the numbers without the offset are very slightly to much to the top
compare vertical offset

But then I tried it with a different font size. Again with offset left, without right ... and now the one without is correct and
the one with is false.
grafik

And compared with different fontsize but in both cases with the 3 +
grafik

  • With offset I mean the 3 + I added

Sorry, I missed this. I sent a PR to your project with a possible fix: simmihugs/codeEditor#1.

I think the complication you were having was caused by generating the text positions manually. The fix does two things:

  • Reuses the bounding boxes of the text lines.
  • Considers the descending of the font.

I tried with a few font sizes (small and really large) and it looks good for me.

Looks great I merged the request - it looks great.

Great!

I'm not sure how you handle the pull requests you receive, but you can test them locally before merging them to your repository to ensure the changes work as expected. This page shows how you can check out a PR. I don't personally use GitHub's CLI, so I follow the (cumbersome) steps listed in "Modifying an inactive pull request locally" (without the push part).

I'll close the issue now. Please re-open or create a new one if needed. Thanks!