Skip to content

What is the proper way to lazily add styles to paragraphs in RichTextFX? #1273

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
PavelTurk opened this issue Apr 24, 2025 · 6 comments
Open

Comments

@PavelTurk
Copy link
Contributor

Suppose I have a document with a huge number of paragraphs (e.g., 10,000,000+). It makes no sense to add styles to all paragraphs at once for performance reasons. What is the correct approach to:

  • Apply styles only when paragraphs become visible (e.g., during scrolling).
  • Avoid showing unstyled paragraphs (ensure styles are applied before rendering).

Is there a built-in mechanism in RichTextFX for this, or do I need a custom solution?

@PavelTurk
Copy link
Contributor Author

PavelTurk commented Apr 25, 2025

For example in JavaFX CodeArea they use decorator:

        codeArea.setSyntaxDecorator(new SyntaxDecorator() {
            @Override
            public RichParagraph createRichParagraph(CodeTextModel ctm, int i) {
                System.out.println("LINE: " + i);
                StyleAttributeMap a = StyleAttributeMap.builder().setBold(true).build();
                RichParagraph.Builder b = RichParagraph.builder();
                b.addSegment(ctm.getPlainText(i), a);
                b.addHighlight(19, 4, Color.rgb(255, 128, 128, 0.5));
                b.addHighlight(20, 7, Color.rgb(128, 255, 128, 0.5));
                return b.build();
            }

            @Override
            public void handleChange(CodeTextModel ctm, TextPos tp, TextPos tp1, int i, int i1, int i2) {

            }
        });

createRichParagraph method is called every time when view is updated. For example, when user adds one character about 110 lines are updated and for every paragraph createRichParagraph is called. Is it possible to have a handler that will be called on paragraph update in RichTextFX?

@Jugen
Copy link
Collaborator

Jugen commented Apr 25, 2025

Yes, have a look at JavaKeywordsDemo

@PavelTurk
Copy link
Contributor Author

@Jugen Thank you very much for your help. Do I understand correctly - you are talking about this one:

        // recompute syntax highlighting only for visible paragraph changes
        // Note that this shows how it can be done but is not recommended for production where multi-
        // line syntax requirements are needed, like comment blocks without a leading * on each line. 
        codeArea.getVisibleParagraphs().addModificationObserver
        (
            new VisibleParagraphStyler<>( codeArea, this::computeHighlighting )
        );

@Jugen
Copy link
Collaborator

Jugen commented Apr 25, 2025

Yes :-)

@PavelTurk
Copy link
Contributor Author

@Jugen Thank you very much. This is what I was looking for. The only problem it doesn't work as expected :).

This is my code - the idea is very simple to make every paragraph red:

class ParagraphStyler implements
        Consumer<ListModification<? extends Paragraph<Collection<String>, String, Collection<String>>>> {

    private final org.fxmisc.richtext.CodeArea area;

    private final BiFunction<Integer, String, StyleSpans<Collection<String>>> styler;

    private int prevParagraph, prevTextLength;

    public ParagraphStyler(org.fxmisc.richtext.CodeArea area, BiFunction<Integer, String, StyleSpans<Collection<String>>> styler) {
        this.styler = styler;
        this.area = area;
    }

    @Override
    public void accept(ListModification<? extends Paragraph<Collection<String>, String, Collection<String>>> lm) {
        if (lm.getAddedSize() > 0) Platform.runLater(() -> {
            int paragraph = Math.min(area.firstVisibleParToAllParIndex() + lm.getFrom(),
                    area.getParagraphs().size() - 1);
            String text = area.getText(paragraph, 0, paragraph, area.getParagraphLength(paragraph));
            if (paragraph != prevParagraph || text.length() != prevTextLength) {
                if (paragraph < area.getParagraphs().size() - 1) {
                    int startPos = area.getAbsolutePosition(paragraph, 0);
                    area.setStyleSpans(startPos, styler.apply(paragraph, text));
                }
                prevTextLength = text.length();
                prevParagraph = paragraph;
            }
        });
    }
};

public class CodeAreaTest extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        CodeArea codeArea = new CodeArea();

        var red = Collections.singleton("red");

        BiFunction<Integer, String, StyleSpans<Collection<String>>> styler = (paragraphNumber, paragraphText) -> {
            System.out.println("LINE: " + paragraphNumber + ", text: " + paragraphText);
            StyleSpansBuilder<Collection<String>> spansBuilder = new StyleSpansBuilder<>();
            spansBuilder.add(red, paragraphText.length() + 1); // 1 - EOL
            return spansBuilder.create();
        };
        codeArea.getVisibleParagraphs().addModificationObserver(new ParagraphStyler(codeArea, styler));
        VirtualizedScrollPane scrollPane = new VirtualizedScrollPane(codeArea);

        Scene scene = new Scene(scrollPane, 1000, 600);
        String css = ".red { -fx-fill: red; } ";
        scene.getStylesheets().add("data:text/css," + css);

        primaryStage.setScene(scene);
        primaryStage.show();

        String content = getContent();
        codeArea.appendText(content);
    }

    private String getContent() throws Exception {
        String url = "https://raw.githubusercontent.com/openjdk/valhalla/e1280b3e11a98d98c0fdad73ce9c8bb9d2417a70/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/JavacParser.java";
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .build();
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
        String content = response.body();
        return content;
    }

    public static void main(String[] args) {
        launch(args);
    }

}

And this is result:

Image

As you can see, the number of unpainted lines at the end of the document is increasing. The console data shows that not all lines are being passed to the styler. I use RichTextFX 0.11.5. Could you say how to fix it?

@PavelTurk
Copy link
Contributor Author

@Jugen Could you say, what is the proper way to update styles when this approach is used? For example when it is necessary to update only styles for paragraphs 1 - 100? I tried this way:

var green = Collections.singleton("green");
        var button = new Button("Test");
        button.setOnAction(e -> {
            for (var i = 0; i < 100; i++) {
                var p = codeArea.getParagraphs().get(i);
                StyleSpansBuilder<Collection<String>> ssb = new StyleSpansBuilder<>();
                ssb.add(green, p.getText().length() + 1);
                p.restyle(0, ssb.create());
            }
            System.out.println("Updated");
        });

but it didn't work. I also tried to use absolute position and codeArea.setStyleSpans(startPos, styles); but it didn't work either.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants