Skip to content
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

Trackpad scrolling with momentum doesn't work on OS X. #265

Closed
StrangeNoises opened this issue Mar 2, 2016 · 20 comments · Fixed by FXMisc/Flowless#64
Closed

Trackpad scrolling with momentum doesn't work on OS X. #265

StrangeNoises opened this issue Mar 2, 2016 · 20 comments · Fixed by FXMisc/Flowless#64
Labels
bug dependency OS-Mac Issues specific to the Mac platform

Comments

@StrangeNoises
Copy link

I noticed this first with markdown-writer-fx but was able to repeat it just with the Java Keywords Demo after pasting in a source file big enough to scroll around in, therefore reporting it here. On markdown-writer-fx the left-side editor pane, implemented using RichTextFX, exhibits this problem, but in the right-hand pane, which is the non-editable preview output, scroll behaviour works normally, so it looks like it's just the RichTextFX pane.

Basically, with two-finger scrolling with a trackpad one expects the movement to have some momentum to it, so you push, or swipe up or down, with two fingers and release in mid-movement, it glides to a stop. There is none of this in the RichTextFX window/pane, so you can only scroll in short, sharp steps a few lines at a time in exact lockstep with the fingers sliding on the surface. Unsurprisingly this gets old fast. 😉

This does not appear to be the case in Windows (even when it's running as a VM on the mac, in VMWare). Only when running directly in OS X. However it may only be apparently working on Windows because the mouse/pointer events are being mediated by the virtual, emulated, mouse driver, which I'm guessing may be resolving such momentum effects into discrete events. I'm unable to test this on a natively-running Windows, as I have no such beast here.

I don't think it's a general Java FX scrolling issue as, as mentioned, the right-hand preview pane in markdown-writer-fx has normal with-momentum scrolling behaviour that feels completely consistent with that of native Mac apps. Also other JavaFX apps with scrollable areas such as the ControlsFX fxsampler also seem to have normal two-finger scrolling momentum behaviour. So it looks like it might be a difference in the way such scroll events are handled in RichTextFX. e.g.: if a scrolling event that normally works correctly by default is being intercepted and handled completely, losing that expected behaviour along the way.

Scrolling speed and redraw performance itself seems fine; if for instance I just click-drag the scrollbar, everything updates as fast and as smoothly as I could wish it, (although this is a quick computer, I haven't tried it on my RPi2 yet 😉) so it doesn't appear to be just being slow.

... additional observation, noticed just before clicking submit:

two-finger trackpad scrolling is of course 2D. i.e.: you can pan and scan right and left as well as up and down. In the Java Keywords Demo, with a source that has long (too long for Java style guides!) lines of code in it, making the window's logical width significantly wider than its visible width, the X-axis scrolling is behaving normally. I can two-finger swipe left or right and it slides to a halt exactly as I'd expect; but Y-axis is still stuck to my exact finger movements.

@JordanMartinez
Copy link
Contributor

This issue doesn't arise with Linux Mint.

@StrangeNoises
Copy link
Author

StrangeNoises commented Apr 22, 2016

This problem has gone away for me in OSX, so I'm going to close this. Scrolling using magic trackpad with a big java source pasted into the JavaKeywords demo is behaving exactly as I would expect.

As before, checking with richtextfx-demos-fat-0.6.10.jar. I didn't specify that version in my original report - probably because I'd already established it was still the case with the then-HEAD. But as 0.6.10 was the current stable release as of the time I submitted the original report, I expect the fix isn't actually in this code but possibly in a more recent release of Java/JavaFX itself. (Currently using 8u92; it's possible it was fixed in 8u77, but I never tested in that timeframe.)

edit: Looks like it was probably this: http://bugs.java.com/view_bug.do?bug_id=8152452 although the symptoms don't exactly match what I was seeing (but in JavaFX rather than Swing, and on a later OSX), but the fix seems definitely to have been in the same ballpark.

@TomasMikula
Copy link
Member

Thanks for reporting back! It would have cost us a lot of time trying to figure out what exactly was going on.

@shoaniki
Copy link
Contributor

shoaniki commented Jun 11, 2016

I literally came here to report this exact bug -- so it does not seem to have gone away for everyone.

I am seeing the behaviour reported above with the versions reported above -- RTFX 0.6.10 and Java 8u92 -- as well as with the latest versions available, i.e. the RTFX-1.0.0 snapshot dated June 10 and the Java 8u102 early access release. This is all with a 2016 MacBook running OS X 10.11.5. And, as observed above, this only happens in OS X itself; everything works as expected within a Windows VM on the same computer.

I did a bit of digging to see whether I could figure out anything about the problem. Installing an event filter shows strange things happening. To be precise, if the scrolling hits the edge of the rich text control, everything happens as expected -- it receives a start scroll event, then a sequence of normal scroll events, then a stop scroll when the fingers are released, and finally a sequence of momentum scroll events after that. But if scrolling does not hit the edge, this sequence is terminated early. I see the start scroll event, then maybe six or seven of the normal scroll events, and then -- nothing. No more scroll events, no end scroll event.

So it looks like something is causing the events to stop being sent at all when the recipient is a rich text control that is scrolling ... that's as far as I've got, since I can't claim to understand low-level JavaFX event handling yet!

@TomasMikula TomasMikula reopened this Jun 11, 2016
@StrangeNoises
Copy link
Author

Sorry for not coming back on this for ages, I was doing other things. now I'm actually working on code that uses this, and I can report I'm seeing the same thing again as I originally reported, and as per shoaniki. I can only guess that it may have been accidentally fixed in a given release of Java 8, and as it was accidental, was broken again in a later one. Although I still also hope that the fix for this resides in the code here, somewhere.

Not got to the point where I need to fix this for myself now, just noting that I am still/again experiencing it.

@JordanMartinez
Copy link
Contributor

If you add an event handler to the scene, and output all the Scroll Event's data, what does it output? (similar to what @shoaniki did beforehand).

I can only guess that it may have been accidentally fixed in a given release of Java 8, and as it was accidental, was broken again in a later one.

Seems like that to me.

@StrangeNoises
Copy link
Author

StrangeNoises commented Nov 22, 2016

Some edits for sense.

Incoming! I had to get to a point in my own coding where I was actually using RichTextFX directly rather than indirectly. (I'm writing Yet Another Markdown Editor (not its name) and for getting-something-working-quickly I was appropriating markdown-editor-fx's MarkdownEditorPane. Now I'm not.) At this stage I'm using a completely basic CodeArea with no syntax colouring rules defined yet.

I added a very very simple event filter like this to the CodeArea editor:

        editor.addEventFilter(ScrollEvent.ANY, se -> {
            LOG.info(se.toString());
        });

I added the same to the resulting WebView preview pane, which is scrolling correctly, ie: with the expected momentum-effect when the fingers are lifted from the trackpad while still in motion. I ran it, with a fairly big markdown document in the window and just did a quick two-finger upward swipe (to scroll down) in each window, the editor first, the preview pane second.

The log is attached.
scroll.txt

What it looks like is that there is an initial SCROLL_STARTED, then a few (5 or 6) SCROLL events, then a SCROLL_FINISHED. Then the CodeArea receives one more SCROLL event and that's it. The WebView recieves a ton more SCROLL events after the SCROLL_FINISHED, which I think represent the momentum effect after the user has lifted their fingers and it slows down to a stop. Indeed during this period the deltaY figures seem to reduce, though not consistently or linearly, but the general trend is there.

NB: The same effect was present when I tried not wrapping the CodeArea in a VirtualizedScrollPane, so it doesn't look like that is directly responsible at least. But is it possible (though I can't see it with a quick look at the source) that something is consuming SCROLL events that come in after a SCROLL_FINISHED, presuming there wouldn't be any?

@StrangeNoises
Copy link
Author

Additional comment:

I turned wrap text off. Sideways scrolling with inertia is working just fine, only vertical scrolling is broken.

Actually I've just noticed I mentioned that in my very original report too, when I raised the issue. Confirming that it's still true, and using a snapshot built from the current code in master. I've been looking through the code, including at Flowless, and I can't see why it might do this, or be doing anything different for X and Y scrolling.

NB: Side-issue: In the master code, as opposed to 0.7 M2, moveTo(pos) seems to not be working. On 0.7 M2, editor.moveTo(0) moved the caret to the top of the document, and scrolled there too. On master, it's doing nothing.

@StrangeNoises
Copy link
Author

StrangeNoises commented Nov 22, 2016

OK, another observation that might be relevant:

With wrap text back on, with a document loaded that has long paragraphs each on one logical line (as opposed to preformatted for text like my earlier test material) I'm finding even normal scrolling without inertia isn't working properly when starting scrolling with the pointer over a long paragraph.

I get a SCROLL_STARTED, and maybe one or two SCROLL events, and then nothing. What I experience as a user is that even with the fingers remaining on the trackpad sliding up and down, I only get a scroll of about one line, then it stops.

It's basically like something is cancelling the scroll sequence. I was looking for a way to do that deliberately in the JavaFX API but didn't find it. But that's what it looks like. Long paragraphs make the problem even worse. The longer the paragraph, the quicker it gives up. (edit: scratch that, it doesn't seem to be a reliable pattern, more random than that.)

It doesn't seem to be a problem when actually dragging the scroll bar.

I plugged in a mouse. That seems fine too, but the event behaviour seems different anyway: There are no SCROLL_STARTED events, and of course no inertia events (where ScrollEvent.isInertia() is true), just a succession of separate SCROLL events.

@StrangeNoises
Copy link
Author

I've narrowed this down quite a lot. I stuck an event filter into lots of places to log scroll events.

(I also by the way set the real event handler in VirtualFlow to receive ScrollEvent.ANY instead of just ScrollEvent.SCROLL as we want the SCROLL_STARTED and SCROLL_FINISHED events too. That didn't fix it, but I believe is the right thing to do anyway.)

All the events are being received by ParagraphBox and ParagraphText. What happens is that they get lost when that ParagraphBox is orphaned from the Navigator they're in, because it's been scrolled off the top of the viewport. As soon as that happens, the ParagraphBox's parent becomes null and the events no longer bubble up to the VirtualFlow for it to perform the scroll.

I think if you can keep the ParagraphBox instances as children of the Navigator after they've left the viewport, at least until the whole scroll gesture is finished, that might help a lot. I'm not certain that accounts for all the trackpad scroll anomalies I encounter (eg: sometimes where I can barely scroll up or down by a line before it dies), but it is certainly stopping inertial scrolling.

Log attached below, where I basically stuck this code:

        this.addEventFilter(javafx.scene.input.ScrollEvent.ANY, se -> {
            java.util.logging.Logger.getGlobal().info(this.getClass().getName()+": ScrollEvent: "+se.getEventType()+", isInertia="+se.isInertia()+", target:("+se.getTarget().getClass().getName()+"), deltaX:Y: "+se.getDeltaX()+":"+se.getDeltaY());
        });

... into the constructors of VirtualizedScrollPane, StyledTextArea, VirtualFlow, Navigator, ParagraphBox, TextFlowExt. You can see as soon as ParagraphBox's parent goes null, unsurprisingly, the events no longer bubble up to everything above that. In the final run, the event filter for ParagraphBox just has the bit extra to show the parent.

scroll-events.txt

@StrangeNoises
Copy link
Author

addendum after a bit more playing. Actually I think that is the whole problem, though I may also have helped a little by having VirtualFlow handle ScrollEvent.ANY, as otherwise you'd lose the first movement which would probably make it feel sluggish. But otherwise, I can now see that if I'm paying attention to which paragraph the pointer is over when I start a trackpad scroll, I can see that it's exactly when that paragraph goes out of the viewport that the scrolling stops.

@StrangeNoises
Copy link
Author

StrangeNoises commented Nov 25, 2016

I've had partial success in fixing this by catching scroll events in an event filter in Navigator, recording some state, then when it's about to crop cells, asking if there's a scroll event ongoing. Literally:

    private boolean isScrollGestureOngoing() {
        return scrollStarted && (!scrollFinished || lastScrollAtTime > System.currentTimeMillis()-1000);
    }

Partial success: It works for scrolling down the document (pushing up) but getting a redraw issue when scrolling up (pushing down) where the items that would have been cropped instead get stuck at the bottom of the window, overlaying each other like a pile of dead flies at the bottom of a double-glazed window, until the next event of some other kind causes them to be cropped. Problem looks like those items' positions are being clamped inside the viewport, but only when scrolling in that direction, not the other. But otherwise it's working, because the events are bubbling up properly. ;-)

@JordanMartinez
Copy link
Contributor

@StrangeNoises Nice detective work there! Still, if this only arises on OSX, that still seems to be a JavaFX issue. We might be able to workaround that using the approach you're using now, but the fix for Java would be best.

@StrangeNoises
Copy link
Author

(tap tap) is this thing on? ;-) Oh wait, your other reply just came in. I'll send this anyway...

OK, I have an alternative fix, again all in Flowless: We allow the cells to be cropped, but we set ScrollEvent handlers on the Cell's Node, which fire the event directly onto the VirtualFlow, that's known to us now separately rather than through Node ancestry.

This appears to work, with no apparent visual defects. Mouse-scrollwheel and scrollbar scrolling appears unaffected, as I'd hope as those events don't go to the Cell nodes anyway. I set the event handlers in this manner rather than using node.addEventHandler() as I was concerned about possible resource leakage; that the same event handler might get added many times in the lifetime of a given cell's node instance. If I'm wrong to worry about that, just using node.addEventHandler() would be more concise.

I can put together a PR if you're OK with this as an approach, unless you get a better idea. (Personally I'd feel that the first solution - not cropping the scrolled-out cells until the scroll is finished - would be neater, but I fear that might make a lot of work in the layout code. What it looks like there is just that the cell's position is not shifted when it needs to be, from its last position before leaving the viewport.)

diff --git a/src/main/java/org/fxmisc/flowless/VirtualFlow.java b/src/main/java/org/fxmisc/flowless/VirtualFlow.java
index d4b269a..aaf61fb 100644
--- a/src/main/java/org/fxmisc/flowless/VirtualFlow.java
+++ b/src/main/java/org/fxmisc/flowless/VirtualFlow.java
@@ -77,7 +77,7 @@ public class VirtualFlow<T, C extends Cell<T, ?>> extends Region implements Virt
         this.getStyleClass().add("virtual-flow");
         this.items = items;
         this.orientation = orientation;
-        this.cellListManager = new CellListManager<>(items, cellFactory);
+        this.cellListManager = new CellListManager<>(this, items, cellFactory);
         MemoizationList<C> cells = cellListManager.getLazyCellList();
         this.sizeTracker = new SizeTracker(orientation, layoutBoundsProperty(), cells);
         this.cellPositioner = new CellPositioner<>(cellListManager, orientation, sizeTracker);
@@ -91,7 +91,7 @@ public class VirtualFlow<T, C extends Cell<T, ?>> extends Region implements Virt
         lengthOffsetEstimate = sizeTracker.lengthOffsetEstimateProperty().asVar(this::setLengthOffset);
 
         // scroll content by mouse scroll
-        this.addEventHandler(ScrollEvent.SCROLL, se -> {
+        this.addEventHandler(ScrollEvent.ANY, se -> {
             scrollXBy(-se.getDeltaX());
             scrollYBy(-se.getDeltaY());
             se.consume();
diff --git a/src/main/java/org/fxmisc/flowless/CellListManager.java b/src/main/java/org/fxmisc/flowless/CellListManager.java
index 11a76e8..2e5421b 100644
--- a/src/main/java/org/fxmisc/flowless/CellListManager.java
+++ b/src/main/java/org/fxmisc/flowless/CellListManager.java
@@ -5,6 +5,7 @@ import java.util.function.Function;
 
 import javafx.collections.ObservableList;
 import javafx.scene.Node;
+import javafx.scene.input.ScrollEvent;
 
 import org.reactfx.EventStreams;
 import org.reactfx.Subscription;
@@ -13,7 +14,7 @@ import org.reactfx.collection.MemoizationList;
 import org.reactfx.collection.QuasiListModification;
 
 final class CellListManager<T, C extends Cell<T, ?>> {
-
+    private final Node owner;
     private final CellPool<T, C> cellPool;
     private final MemoizationList<C> cells;
     private final LiveList<C> presentCells;
@@ -22,8 +23,10 @@ final class CellListManager<T, C extends Cell<T, ?>> {
     private final Subscription presentCellsSubscription;
 
     public CellListManager(
+            Node owner,
             ObservableList<T> items,
             Function<? super T, ? extends C> cellFactory) {
+        this.owner = owner;
         this.cellPool = new CellPool<>(cellFactory);
         this.cells = LiveList.map(items, this::cellForItem).memoize();
         this.presentCells = cells.memoizedItems();
@@ -84,10 +87,21 @@ final class CellListManager<T, C extends Cell<T, ?>> {
         // Make cell initially invisible.
         // It will be made visible when it is positioned.
         node.setVisible(false);
+        // catch scroll events and send them to the VirtualFlow,
+        // bypassing normal event bubbling that breaks when
+        // a scrolling object is sent out of the viewport.
+        node.setOnScroll(this::forceBubbleScrollEvent);
+        node.setOnScrollStarted(this::forceBubbleScrollEvent);
+        node.setOnScrollFinished(this::forceBubbleScrollEvent);
 
         return cell;
     }
 
+    private void forceBubbleScrollEvent(ScrollEvent se) {
+        owner.fireEvent(se);
+        se.consume();
+    }
+
     private void presentCellsChanged(QuasiListModification<? extends C> mod) {
         // add removed cells back to the pool
         for(C cell: mod.getRemoved()) {

@StrangeNoises
Copy link
Author

I don't think it is a JavaFX issue. The issue is because the cell nodes on which the events are coming in are being orphaned from their parent and the event isn't bubbling up. Given the nature of trackpad gesture events, I don't think they're going to change the fact the events land in the cell being pushed. It is, by design, exactly analogous to putting ones finger on a piece of paper on one's desk and shoving it. And in that respect it's normal, for instance, to shove an item out of the viewport, and without lifting one's fingers, bring it back in as well. I don't think this is something they can fix in JavaFX internals.

@JordanMartinez
Copy link
Contributor

@StrangeNoises It's Thanksgiving / Black Friday in the US, so that might explain why some haven't been as active this past week.

I think it's a Mac-specific JavaFX issue because the code as it is now (0.7-M2 release) works without issue on my operating system (Linux). The scrolling has inertia whether I scroll up or down on the first/last displayed paragraph. If it was an issue with the code, why can't I reproduce the bug? Am I doing something wrong in my test?

@StrangeNoises
Copy link
Author

Thanksgiving? Pah! 🦃🔪 (Spot the non-American.)

Pretty sure this isn't a bug, and no-one on Mac JavaFX dev would be persuaded it is, as it's giving more information about a scroll gesture ongoing than you can get from a mouse, but which is useful in a trackpad/touchscreen type of setting - including which Node the gesture started on, what thing you have your finger on while you're pushing, in case, for instance, you want that thing to visually react in some way.

It only breaks Flowless because it's breaking an assumption that you can't get events for Nodes that have left the viewport. Navigator is aggressively cropping out cells as soon as they're out of the viewport, thus depriving them of a Parent they can bubble up events through.

On Windows & Linux how trackpad actions are processed into events that apps can see may be driver, or especially in Linux's case, driver config dependent. (I just took a couple of scary minutes to look at the Arch Touchpad Synaptics wiki page. There seems to be a lot that could go wrong!) It may for instance be handling gestures and inertial effects internally and flattening events given to the window manager to be mouse-compatible. Like VMWare does, as the guest only sees an emulated mouse whatever I'm using on the outside. JavaFX can only work with the events it's given. But trackpads and drivers may not all be equal. Also, consider which behaviour is more likely in a tablet context. Like an MS Studio device. (Ooh! Do I have an excuse to buy one suddenly? 😎 Shame to only be thinking so as Black Friday is coming to an end here...)

The way to see what events you're getting is just to log the event in the VirtualFlow scroll handler you already have. But also change it to catch ScrollEvent.ANY, not just ScrollEvent.SCROLL. And look at the target, and the "inertia" flag.

On a mouse, or if the trackpad driver is emulating a mouse, you won't get SCROLL_STARTED or SCROLL_FINISHED, and you won't see "inertia" events (ie: with isInertia() == true), even if the deltas look like an inertial motion, and the target for each event during the gesture will be different, the Node directly under the pointer when that event fired. I expect that's what you'll see, as it would explain how it appears to work.

Put a similar event logger on TextFlowExt; or you can just add it in CellListManager.cellForItem like I did with my second proposed fix. That'll tell you about any events that are being received but not getting bubbled up to the VirtualFlow. Because it's possible you are losing some events, but for whatever reason it's just not as prominent.

@JordanMartinez
Copy link
Contributor

@StrangeNoises Since you seem to have already added many event listeners throughout RTFX, could you fork the project and create a branch with those items already added? Then I can create a local branch of your work, test it out on my end, and I can then post the results I get when I scroll around.

@StrangeNoises
Copy link
Author

In the end the only changes I needed to make were in Flowless. That's where the problem is, because of how it aggressively removes child nodes of the Navigator when they're out of view. I've therefore already forked that project for my own purposes here: https://github.com/StrangeNoises/Flowless

I just now added two lines to it to log (at level FINE) the events received both by the cell nodes (in CellListManager) and the VirtualFlow itself. To disable the fix and still get the logging it's only necessary to comment out the other two lines in the CellListManager.pushScrollEvent() method.

At the weekend, meanwhile, I installed ArchLinux on my Macbook Pro. I was never able to reproduce inertial scrolling (or as it seems to be called there, "kinetic" scrolling) in my JavaFX application, with or without my fix. In fact it only happens at all in some Gnome3 apps.

I think this is because, having a new install, everything defaulted to using libinput on gnome on wayland. I tried various other window managers but never got any "kinetic" scrolling on any of them at all, in any app.

(I was actually trying the other window managers more in search of decent HiDPI support, ended up back on Gnome/Wayland as the best of a bad bunch! JavaFX in particular seems to have no automatic awareness of HiDPI on Linux as it does already on both macOS and Windows10. I believe it may be possible to do it explicitly in the app though, but I'll have to dig that out again from where I think I saw it once; and hopefully it'll become unnecessary with time anyway.)

I suspect you're using the synaptics driver which, I read elsewhere - here: https://wayland.freedesktop.org/libinput/doc/latest/faq.html - my understanding is that the synaptics driver is handling "kinetic" scrolling internally and just delivering ordinary scroll events for everything, which matches my generic guess beforehand. Therefore you will get each consecutive scroll event for the element directly under the pointer at that moment, and therefore it'll work. (but if you wanted to make use of scroll gesture awareness, you couldn't.)

Libinput drivers apparently don't have a specific concept of kinetic/inertial scrolling, rather having the idea of "scroll sources", which need to be interpreted by the client (which I think from that point of view means the window manager, or possibly even the application, via whatever API layer its using) So I think in my case kinetic scrolling is working for Gnome3/GTK3 apps as that API layer is doing it. I think JavaFX is using GTK2 still, and along with many many other apps, just doesn't know anything about it. ISTR somewhere JavaFX9 may be going to use GTK3, in which case maybe this fixes itself then - but probably only for libinput users!

... Which all supports my earlier guess that it's all highly driver - or alternatively window-manager - dependent. First JavaFX has to be able to receive and understand the OS-level events that indicate a scroll gesture, as distinct from separate scroll events for each movement, for it to treat it as such, and it's not, at present, getting these from libinput, and I'm guessing it's not getting such events from synaptics either.

It's my assumption that when JavaFX gets scroll-gesture type events it will turn them into the same kind of ScrollEvents seen in JavaFX on macOS, and that as such it's not a macOS-specific JavaFX bug, but rather how it's intended to work; but that is an assumption and we just haven't yet seen scroll gestures supported properly on the other platforms.

In any case it all feels outside the purview of us poor bloody Java app developers to fix at this stage, so I'm content for now with just having my own fork of Flowless that fixes the problem on macOS for me. If you start getting more complaints from Mac users of apps using Flowless, then it may become more worth your time to accept the patch; otherwise I'm content to sit on it and just use it for myself for now.

FWIW also installed Windows natively, but haven't yet got my Apple trackpad working on it at all. (But that's how I know the HiDPI works at least.)

@JordanMartinez
Copy link
Contributor

This issue needs to be reexamined since it was never resolved. Unfortunately, this seems to be a problem with Flowless, not RichTextFX. Since I'm not as familiar with Flowless, I'm not sure what I can do to help resolve it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug dependency OS-Mac Issues specific to the Mac platform
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants